# Gradients & Effects

_Box Kite 2.2.6 · a markdown copy of https://www.box-kite.dev/gradients-shadows/_

A gradient written as a value, so its stops are palette colours — four shadows that stack on one box-shadow, and nine filter functions that stack the same way.

## A gradient is a value, not a string

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.

**The three kinds**

```jsx
<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'] }} />
```

## Direction, shape, angle

`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.

**One of each**

```jsx
<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>
```

## A stop is a colour, and every colour value works

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.

**Positions and the opacity modifier**

```jsx
<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>
```

## The space the colours travel through

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.

**The same two stops, three ways**

```jsx
<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>
```

## Judged whole, so a typo shows nothing rather than something else

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.

**Each of these emits nothing at all**

```jsx
<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 */}
```

## Four shadows, one property

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 elevation, from `xxs` to `xxl`; the older `small`, `medium` and `large` presets still work and carry their own colour.

**The elevation scale**

```jsx
<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>
```

## A ring is not an outline

`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.

**Stacked, not replaced**

```jsx
<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>
```

## Every layer has a colour of its own

`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.

**A coloured elevation**

```jsx
<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>
```

## And the same again for text

`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.

**textShadow**

```jsx
<P fontSize={28} fontWeight={700} textShadow="md" textShadowColor="indigo-500/40">
  Raised type
</P>
```

## Why the layers are registered

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.

## Nine functions, one filter

`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.

**The same tile, filtered**

```jsx
<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} />
```

## A number is the function's own unit

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 a named 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.

## A drop shadow follows the shape

`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.

**box-shadow and drop-shadow on the same triangle**

```jsx
<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>
```

## And the same nine behind the element

`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.

**Glass over a gradient**

```jsx
<Box backdropBlur="sm" backdropSaturate={180} bgColor="white/20" insetRing={1} insetRingColor="white/40">
  Frosted
</Box>
```

## A mask is a gradient again

`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.

**Fading an edge**

```jsx
<Box
  bgGradient={{ linear: 'r', colors: ['sky-500', 'indigo-600'] }}
  maskImage={{ linear: 'b', colors: ['black', ['black', '55%'], 'transparent'] }}
/>
```

## And a gradient can be the letters

`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.

**Gradient type**

```jsx
<H2 fontSize={40} fontWeight={800} color="transparent" bgClip="text" bgGradient={{ linear: 'r', colors: ['violet-500', 'cyan-400'] }}>
  Painted by the background
</H2>
```

---

_Every page: https://www.box-kite.dev/llms.txt · every prop, measured: https://www.box-kite.dev/props.md_

