05 · Modern CSS Through Tailwind¶
CSS has gained a remarkable amount in the last few years, and Tailwind v4 exposes much of
it directly: container queries, :has(), subgrid and @starting-style were covered
earlier. This lesson covers more: gradient interpolation, balanced text, text shadows,
masks, 3D transforms, and anchor positioning (through arbitrary properties). Each is
shown with what Tailwind generated and what Chromium 149 rendered, plus how to use it
safely when not every browser has it yet.
Gradients and colour interpolation¶
v4's gradient utilities are bg-linear-*, bg-radial and bg-conic, with colour stops
from from-*, via-* and to-*. An optional modifier picks the colour space the
gradient is interpolated in:
<div class="bg-linear-to-r/srgb from-blue-600 to-yellow-400"></div>
<div class="bg-linear-to-r from-blue-600 to-yellow-400"></div> <!-- default -->
<div class="bg-linear-to-r/oklch from-blue-600 to-yellow-400"></div>
The plain class already isn't sRGB. Compiled:
.bg-linear-to-r {
--tw-gradient-position: to right;
@supports (background-image: linear-gradient(in lab, red, red)) {
--tw-gradient-position: to right in oklab;
}
background-image: linear-gradient(var(--tw-gradient-stops));
}
So by default, browsers that support interpolation spaces blend in oklab. Older ones fall back to the classic behaviour. We screenshotted each 400px bar and read the pixel at the midpoint:
/srgb midpoint rgb(137, 146, 91) muddy olive
default midpoint rgb(140, 162, 172) greyish blue (oklab)
/oklch midpoint rgb(0, 198, 156) vivid teal
Blue to yellow passes through grey in sRGB and oklab, because they're roughly opposite
colours and the straight line between them crosses the neutral axis. oklch interpolates
hue around the colour wheel instead, keeping colourfulness, so the middle is a
saturated teal. Which one is "right" depends on the design. For gradients between
distant hues, try /oklch; for subtle gradients between similar colours, the default
is fine. /[in_hsl_longer_hue] and other arbitrary interpolation methods also work.
Angles and positions: bg-linear-45, bg-linear-to-br, bg-radial-[at_25%_25%],
bg-conic-180.
Balanced and pretty text¶
text-balance (text-wrap: balance) evens out line lengths in short text such as
headings. We measured the line widths of a text-3xl heading in a 300px column:
"Weekend sailing courses on the south coast"
default 228px, 292px, 78px <- "coast" alone on the last line
text-balance 228px, 206px, 164px
"How we cut our CSS bundle in half"
default 283px, 188px
text-balance 216px, 255px
It doesn't always change anything: for some headings the default breaks were already even, and the measured widths were identical. Browsers limit balancing to a few lines, so use it on headings, captions and short labels, not paragraphs.
text-pretty (text-wrap: pretty) is for body text: it asks the browser to avoid
leaving a single short word on the last line of a paragraph, with less cost than full
balancing. Both are progressive enhancements; unsupported browsers just wrap normally.
Text shadows¶
text-shadow-md compiled to three layered shadows using
var(--tw-text-shadow-color, rgb(0 0 0 / 0.1)), so a colour utility like
text-shadow-sky-500/50 recolours all layers. The practical use is legibility of text
over images; as decoration, text shadows date quickly.
Masks¶
Mask utilities fade or cut out an element without editing the image:
<!-- fade the bottom half of a long preview -->
<div class="max-h-48 overflow-hidden mask-b-from-50%">…long text…</div>
<!-- soft circular vignette on a photo -->
<img class="mask-radial-from-40%" src="…" alt="…">
They compile to mask-image built from internal variables, combined with
mask-composite: intersect, so several mask utilities on one element combine (a bottom
fade and a radial vignette). A masked region is still there for screen readers and
text selection; masking is purely visual.
3D transforms¶
<div class="group perspective-distant">
<div class="relative size-48 transition-transform duration-500 transform-3d
group-hover:rotate-y-180">
<div class="absolute inset-0 grid place-items-center rounded-xl bg-sky-700 text-white
backface-hidden">Front</div>
<div class="absolute inset-0 grid place-items-center rounded-xl bg-gray-900 text-white
rotate-y-180 backface-hidden">Back</div>
</div>
</div>
A card that flips on hover: perspective-* on the parent sets the viewing distance,
transform-3d (transform-style: preserve-3d) lets children live in 3D, and
backface-hidden hides each face when it's turned away. Note that 3D rotations
(rotate-y-180) use the transform property, not rotate (Level 2 · 09), and they're
covered by transition-transform. Add motion-safe: to the hover rotation; a flip is a
large movement. And remember that hover doesn't exist on touch screens, so the back face
needs another way to be shown there (a button, or focus-within).
Anchor positioning (with arbitrary properties)¶
CSS anchor positioning lets an element position itself relative to another element anywhere on the page, without JavaScript measuring. Tailwind 4.3 has no dedicated utilities for it, but arbitrary properties and values cover it:
<button popovertarget="menu" class="[anchor-name:--menu]">Options</button>
<div id="menu" popover
class="[position-anchor:--menu] top-[anchor(bottom)] left-[anchor(left)] mt-1 m-0
w-48 rounded-lg bg-white p-1 shadow-lg ring-1 ring-black/5">
…
</div>
In Chromium 149 our test (with an absolutely positioned panel) placed the panel's left
edge exactly at the button's left (100px) and its top 4px below the button's bottom
(148px vs 144px), the mt-1 gap. Anchor positioning support is still uneven across
browsers, so this is the textbook case for a feature check.
Feature checks with supports-*¶
supports-[…]: wraps a utility in @supports, and not-supports-[…]: in its negation:
<div class="absolute top-full left-0
supports-[anchor-name:--x]:[position-anchor:--menu]
supports-[anchor-name:--x]:top-[anchor(bottom)]">…</div>
The pattern is fallback first, enhancement in supports-*. Many modern features
need no check at all because unsupported declarations are simply ignored (text-balance,
field-sizing-content). Use supports-* when the fallback needs different declarations
(a different position, a different display), as with anchor positioning.
Choosing what to adopt¶
A quick rule for each new feature:
- Ignored safely when unsupported (text-wrap, most new colour functions inside
@supportslike Tailwind's gradients): use freely. - Breaks layout when unsupported (anchor positioning, some container query units
used for critical sizing): use with a
supports-*fallback. - Breaks functionality when unsupported: don't rely on it yet, or polyfill.
Check support for your actual audience (your analytics, plus a compatibility table such as MDN's or caniuse) rather than guessing.
How It Actually Works¶
Tailwind is a thin layer here. For gradients it builds --tw-gradient-stops from your
from/via/to utilities and puts the interpolation method into
--tw-gradient-position, guarded by an @supports check so older browsers get a valid
gradient. For text wrapping, shadows, masks and 3D, each utility maps to one or two
properties, plus internal variables when several utilities need to combine (masks, text
shadow colours). The browser does all the work, which is why behaviour such as how
text-balance chooses breaks, or which colour the oklch midpoint lands on, depends on
the browser and its version.
For anything without a utility, arbitrary properties ([anchor-name:--menu]) and values
(top-[anchor(bottom)]) pass the CSS through unchanged. Tailwind doesn't need to know
about a feature for you to use it.
Common mistakes¶
- Assuming
bg-linear-*interpolates in sRGB. The default is oklab; add/srgbto match a design tool that uses sRGB. text-balanceon paragraphs. It's for short text.- Hover-only 3D flips with no touch or keyboard alternative.
- Using anchor positioning without a fallback when your audience includes browsers that lack it.
- Masks to hide content from users. Masked content is still read and selectable.
- Waiting for a utility before using a new CSS feature. Arbitrary properties work now.
Exercise¶
- Make three versions of a gradient between two distant hues (
/srgb, default,/oklch) and decide which suits your design. - Add
text-balanceto every heading in the Level 1 landing page. Use DevTools to find one that changed and one that didn't. - Build the flip card with
motion-safe:and a keyboard-accessible way to see the back. - Build an anchored popover menu with a
supports-*fallback, and test it with the anchor rules removed to simulate an unsupported browser.