Written as CSS a gradient is text, and text cannot be themed, cannot take a token and cannot share a class. Here it is a record: the key names the kind and carries its geometry, colors are the stops in order. Because a stop is an ordinary colour value, a gradient follows the palette into dark mode, takes the opacity modifier, and two elements asking for the same one still resolve to a single rule.
<Box bgGradient={{ linear: 'r', colors: ['blue-500', 'pink-500'] }} />
<Box bgGradient={{ linear: 135, colors: ['blue-500', 'pink-500'] }} />
<Box bgGradient={{ radial: 'circle', at: 'top left', colors: ['sky-400', 'indigo-900'] }} />
<Box bgGradient={{ conic: 45, colors: ['red-500', 'yellow-500', 'red-500'] }} />linear takes one of eight directions — t, tr, r, br, b, bl, l, tl — or a number, which is an angle in degrees with 0 pointing up. radial takes a shape, conic an angle to start from, and either takes at to say where it is centred. A linear gradient runs in a direction rather than out of a point, so at is not one of its keys.
<Flex gap={4} flexWrap="wrap">
<Box width={40} height={28} borderRadius={2} bgGradient={{ linear: 'r', colors: ['blue-500', 'pink-500'] }} />
<Box width={40} height={28} borderRadius={2} bgGradient={{ linear: 135, colors: ['emerald-400', 'sky-600'] }} />
<Box width={40} height={28} borderRadius={2} bgGradient={{ radial: 'circle', at: 'top left', colors: ['amber-300', 'rose-600'] }} />
<Box width={40} height={28} borderRadius={2} bgGradient={{ conic: 45, colors: ['violet-500', 'cyan-400', 'violet-500'] }} />
</Flex>Anything a colour prop takes is a stop: a palette token, a token with an opacity modifier, a system colour, or a var(--chart-1) somebody else declared. A [colour, position] pair says how far along the gradient the stop sits. Two stops is the minimum, because one stop is a colour rather than a gradient.
<Flex gap={4} flexWrap="wrap">
<Box width={40} height={28} borderRadius={2} bgGradient={{ linear: 'r', colors: [['sky-500', '20%'], ['indigo-700', '80%']] }} />
<Box width={40} height={28} borderRadius={2} bgColor="amber-400" bgGradient={{ linear: 'b', colors: ['black/50', 'transparent'] }} />
</Flex>Two stops are joined by a path, and sRGB — the browser default — runs that path through a desaturated middle. Blue to yellow goes grey on the way. interpolate names a better space: oklch keeps the chroma up the whole way, and oklch-longer takes the long way round the hue circle, which is what turns two stops into a spectrum. This is the half of OKLCH the palette work deliberately left here.
<Flex d="column" gap={3}>
<Box height={12} borderRadius={2} bgGradient={{ linear: 'r', colors: ['blue-600', 'yellow-400'] }} />
<Box height={12} borderRadius={2} bgGradient={{ linear: 'r', colors: ['blue-600', 'yellow-400'], interpolate: 'oklch' }} />
<Box height={12} borderRadius={2} bgGradient={{ linear: 'r', colors: ['blue-600', 'yellow-400'], interpolate: 'oklch-longer' }} />
</Flex>A gradient is one value: a bad stop makes the rest of it meaningless, so the whole record is rejected together and emits no rule and no class name. Unknown keys are rejected too, which vars does not do — these names belong to the grammar, and interpolat misspelt would otherwise paint a silent sRGB gradient with nothing to see.
<Box bgGradient={{ linear: 'r', colors: ['bleu-500', 'pink-500'] }} /> {/* no such token */}
<Box bgGradient={{ linear: 'r', colors: ['blue-500'] }} /> {/* one stop is a colour */}
<Box bgGradient={{ linear: 'r', radial: true, colors: [...] }} /> {/* two kinds at once */}
<Box bgGradient={{ linear: 'r', colors: [...], interpolat: 'oklch' }} /> {/* misspelt key */}CSS gives an element one box-shadow, which normally means the last rule wins and an elevation and a ring cannot coexist. Here each is a layer with a custom property of its own, and all four write the same composed declaration — so they compose the way translateX and translateY do. shadow is Tailwind elevation, from xxs to xxl; the older small, medium and large presets still work and carry their own colour.
<Flex gap={5} flexWrap="wrap">
<Box width={24} height={16} borderRadius={2} bgColor="white" shadow="xs" />
<Box width={24} height={16} borderRadius={2} bgColor="white" shadow="sm" />
<Box width={24} height={16} borderRadius={2} bgColor="white" shadow="md" />
<Box width={24} height={16} borderRadius={2} bgColor="white" shadow="lg" />
<Box width={24} height={16} borderRadius={2} bgColor="white" shadow="xl" />
</Flex>ring and insetRing are a width in pixels rather than a step on a scale. A ring follows borderRadius, costs no layout and joins the shadow stack — all three things an outline does not do — so a focus ring, an inner hairline and an elevation can all be on the same element at once.
<Flex gap={5} flexWrap="wrap">
<Box width={28} height={18} borderRadius={3} bgColor="white" ring={2} ringColor="indigo-500" />
<Box width={28} height={18} borderRadius={3} bgColor="white" insetRing={1} insetRingColor="slate-300" />
<Box width={28} height={18} borderRadius={3} bgColor="white" shadow="lg" ring={3} ringColor="indigo-500/40" />
</Flex>shadowColor, insetShadowColor, ringColor and insetRingColor take every colour value, opacity modifier included. Each shows nothing on its own — a colour with no layer painting it is as inert as borderColor with no border width. none on a shadow, or 0 on a ring, clears just that one layer and leaves the others painting.
<Flex gap={5} flexWrap="wrap">
<Box width={28} height={18} borderRadius={3} bgColor="white" shadow="lg" shadowColor="indigo-500/40" />
<Box width={28} height={18} borderRadius={3} bgColor="white" shadow="lg" shadowColor="emerald-500/40" />
</Flex>textShadow runs xxs to lg with textShadowColor beside it. It is one property with one contributor, so it needs no composing — but it is in the same transition group, and transition="shadow" covers box-shadow and text-shadow both.
<P fontSize={28} fontWeight={700} textShadow="md" textShadowColor="indigo-500/40">
Raised type
</P>The four custom properties are declared with @property in the base stylesheet, and the reason is inheritance: a custom property inherits by default, so a child asking only for a ring would read its parent value through the fallback and wear an elevation nobody gave it. Registering them inherits: false stops that. The universal syntax with no initial value is deliberate too — anything else makes the property always valid, and the fallback carrying each step of its own alpha would never be reached.
filter is a list, and its functions apply in sequence — which is the same problem the shadows have, solved the same way. blur, brightness, contrast, grayscale, hueRotate, invert, saturate, sepia and dropShadow each set a layer of their own and all nine write the same composed declaration, so a blur and a desaturation coexist instead of overwriting each other. The order is the grammar's rather than yours: fixing it is what lets two elements naming the same two functions share one class.
<Box bgGradient={{ linear: 'br', colors: ['amber-400', 'rose-600'] }} blur="xs" />
<Box bgGradient={{ linear: 'br', colors: ['amber-400', 'rose-600'] }} grayscale={100} />
<Box bgGradient={{ linear: 'br', colors: ['amber-400', 'rose-600'] }} hueRotate={140} />
<Box bgGradient={{ linear: 'br', colors: ['amber-400', 'rose-600'] }} blur="xs" saturate={40} />Six of them are a percentage — brightness={110} is ten percent brighter, grayscale={100} removes colour entirely — hueRotate is degrees, and blur is a radius in pixels with Tailwind's scale beside it, xs (4px) through xxxl (64px). none clears one function and leaves the other eight painting, the way none clears one shadow layer.
shadow is cast by the box; dropShadow is cast by what is actually drawn — the outline of an SVG path, the opaque part of a transparent PNG. It is a filter function rather than a fifth shadow layer, which is exactly why the two can be told apart: put both on a triangle and one of them is a rectangle. xs through xxl, with dropShadowColor beside it.
<Svg viewBox="0 0 64 56" width="64px" fill="indigo-500" shadow="xl">
<Polygon points="32,4 60,52 4,52" />
</Svg>
<Svg viewBox="0 0 64 56" width="64px" fill="indigo-500" dropShadow="xl">
<Polygon points="32,4 60,52 4,52" />
</Svg>backdropBlur and its eight siblings filter what is behind the element rather than the element itself, which is the whole of glassmorphism: something translucent in front, the page blurred and pushed behind it. There is a backdropOpacity that filter has no use for and no backdropDropShadow, because neither one means anything on the other side.
<Box backdropBlur="sm" backdropSaturate={180} bgColor="white/20" insetRing={1} insetRingColor="white/40">
Frosted
</Box>maskImage takes the same record bgGradient does, and reads its alpha channel: where the gradient is transparent the element is not painted. A fade to transparent is the whole edge-fade recipe — the one a scrolling list wants at its bottom edge — and because the value is the gradient grammar it takes tokens, positions and interpolation like any other. A url(#id) masks by a shape the document defines. One mask, not a stack.
<Box
bgGradient={{ linear: 'r', colors: ['sky-500', 'indigo-600'] }}
maskImage={{ linear: 'b', colors: ['black', ['black', '55%'], 'transparent'] }}
/>bgClip="text" clips the background to the glyphs, which is how a gradient becomes type. It needs color="transparent" beside it — the text paints over its own background otherwise — and that pairing is deliberately not automatic: bgClip is the CSS property, not a recipe.
<H2 fontSize={40} fontWeight={800} color="transparent" bgClip="text" bgGradient={{ linear: 'r', colors: ['violet-500', 'cyan-400'] }}>
Painted by the background
</H2>