---
language: "en"
title: "Shadows and glows"
canonical: "https://glaze.tenphi.me/shadows-and-glows/"
source: "docs/shadows-and-glows.mdx"
version: "3.1.0"
index: true
---

# Shadows and glows

`shadow` and `glow` are translucent overlays. Each emits a pigment (black for a
shadow, white for a glow) with an opacity Glaze solves so that, once the
browser composites it over `bg`, the surface lands **exactly `intensity`
tones** darker (`shadow`) or lighter (`glow`), scaled by the object when you
name one. The step is the same on every surface and in every scheme where the
surface has room for it: light, mid, dark, and chromatic.

```ts
import { glaze } from '@tenphi/glaze';

const theme = glaze(280, 80);

theme.colors({
  surface: { tone: 97 },
  card: { tone: 100 },
  accent: { tone: 45 },

  // Object-aware drop shadow: `fg` is the element casting it.
  'card-shadow': {
    type: 'shadow',
    bg: 'surface',
    fg: 'card',
    intensity: 8,
  },

  // No `fg`: an exact shade, for state layers and scrims.
  'surface-hover': { type: 'shadow', bg: 'surface', intensity: 5 },
  'accent-pressed': { type: 'shadow', bg: 'accent', intensity: [8, 14] },

  // The mirror: dark-mode elevation, edge highlights, lightening states.
  'accent-highlight': { type: 'glow', bg: 'accent', intensity: 4 },
});
```

This sample resolves without warnings. In dark high contrast the `auto`
surface sits at tone 3, so `card-shadow` and `surface-hover` can't darken it by
their full step and pin to black there. Light high contrast has room for both,
so Glaze doesn't warn. See [Unreachable steps](#unreachable-steps).

Every field is listed in
[Color definitions](/reference/color-definitions/#shadowcolordef--glowcolordef);
this page explains how the overlays behave.

## The same step on every surface

`intensity` is a step on tone, the WCAG-shaped axis Glaze uses everywhere
(see [Tone and OKHST](/okhst/)). For neutrals a tone step is a fixed WCAG
ratio, so 6 tones is about 1.20:1 wherever it lands, and the same number reads
the same on any surface. APCA cannot express steps this small, so overlays
are always measured in tone.

A fixed opacity cannot do this. Below, each surface carries a `shadow` with
`intensity: 6` (top patch) and a black layer at a fixed 8% (bottom patch); the
big number is the tone step each one actually paints.

The solved shadow lands 6.0 tones everywhere, at an opacity that climbs as the
surface darkens. The fixed layer darkens white by almost 6 tones and a tone-12
surface by little more than one. Glows mirror this:

## How the opacity is solved

Browsers composite `source-over` on gamma-encoded sRGB: 50% black over white
paints `#808080`, not the `#bcbcbc` that mixing linear light would give. Glaze
models exactly that. The composite's luminance changes monotonically with
opacity, so a bisection finds the opacity that lands the step, on chromatic
surfaces too.

The required opacity depends heavily on the surface. A 6-tone shadow needs
α 0.081 on white, 0.107 on a tone-50 surface, and 0.477 on a tone-10 surface.

There is no opacity cap: when the step is reachable, it is exact. For a
lighter overlay, lower `intensity`.

The output is the pigment with its solved alpha. Shadows and glows appear in
every output method (`tokens()`, `tasty()`, `css()`, `json()`, and the rest)
alongside regular colors. The default pigment is neutral, so the values stay
clean:

```text
rgb(0 0 0 / 0.0814)       shadow, 6 tones on white
oklch(1 0 0 / 0.0953)     glow, 6 tones on a tone-50 surface
```

The alpha is only meaningful over the surface it was solved for. Paint the
token on top of `bg` (as a `box-shadow` color, a state-layer background, or a
pseudo-element over the surface).

## Direction is fixed per type

A `shadow` darkens in light **and** dark schemes; a `glow` lightens in both.
Neither flips when the scheme does. Pick the type for the effect you want in
each scheme: for example, `shadow` for light-mode cards and `glow` for
dark-mode elevation. When one component needs a darkening layer in light mode
and a lightening one in dark mode, emit both tokens and wire each in the
scheme it belongs to. Both still resolve in all four schemes, so each one warns
where its surface has no room in your light or dark scheme, even if you don't
use it there ([Unreachable steps](#unreachable-steps)).

`bg` and `fg` are read per scheme, so the solved opacity adapts on its own as
the surface moves in dark and high contrast. See
[Light, dark, and high contrast](/schemes/) for how surfaces move between
schemes.

## Object influence

`fg` is the object casting the shadow or emitting the glow, such as the card.
It scales the step the way bounce light does. With `fg`, the step is
`intensity × k`:

```text
k = max(0, 1 + β · d · (t_bg − t_fg) / 100)
```

`t_bg` and `t_fg` are the tones of the surface and the object, `d` is `+1` for
a `shadow` and `−1` for a `glow`, and `β` is `tuning.objectInfluence` (`0.5`
by default).

* An object at the surface's own tone gives exactly `intensity`, and so does
  omitting `fg`.
* A darker object deepens its shadow; a lighter one softens it.
* For a glow it is the other way round: a lighter object glows stronger.
* `k` never drops below 0 and tops out at `1 + β` (a black object casting a
  shadow on a white surface). `objectInfluence: 0` removes `fg`'s effect on
  the step (`k = 1`); `fg` still feeds [`hueBleed`](#tinted-pigment).

Each card below casts a hard-edged `shadow` (offset, no blur) or emits a solid
`glow` ring, both with `intensity: 8` and `fg` pointed at the card:

The same numbers as a table:

| Surface  | Object         | `k` (`shadow`) | `shadow` step | `glow` step |
| -------- | -------------- | -------------- | ------------- | ----------- |
| light 97 | white card 100 | 0.985          | 7.9           | —           |
| light 97 | accent 45      | 1.26           | 10.1          | —           |
| light 97 | near-black 8   | 1.445          | 11.6          | —           |
| dark 12  | raised card 20 | 0.96           | 7.7           | 8.3         |
| dark 12  | accent 55      | 0.785          | 6.3           | 9.7         |

Point `fg` at the element that casts the shadow, never at text: `fg` is the
object, not a contrast reference. Like `bg`, it must not be another shadow or
glow.

## State layers

Without `fg`, a shadow or glow is an exact shade or tint of its surface: the
right tool for hover and pressed states, which need the same visible change on
every fill.

The accent fill uses `mode: 'fixed'`, so it stays a mid tone in every scheme
and its layers land the same steps everywhere. The dark fill uses glows, so
its states lighten it. The neutral `surface` follows the scheme: in dark high
contrast it becomes black, and a shadow has nothing left to darken, so
`surface-hover` and `surface-pressed` stay fully transparent (the figure shows
what they reached). They fit light high contrast, so they don't warn; see
[Unreachable steps](#unreachable-steps). A glow on this surface fails the other
way: it can't lighten the white surface of the light schemes, and warns in
light. No overlay fits a surface that spans the whole
tone range. For a state that darkens in light schemes and lightens in dark
ones, use a regular color relative to the surface, such as
`{ base: 'surface', tone: '-4' }`, which flips with the scheme.

## Card drop shadows

For elevation in light schemes, define one shadow ladder with `bg` on the page
and `fg` on the card. Equal intensity steps read as evenly spaced on every
surface. Use [high-contrast pairs](#high-contrast) for shadows that should
deepen in high contrast.

A blurred `box-shadow` fades out from the token's color, so the step is the
darkest the shadow gets, right at the card's edge. In dark schemes the page
sits near black and a dark card shadow has little room: the solved opacity
climbs, and in dark high contrast the page is black, so all three shadows
stay fully transparent and warn. For elevation in dark schemes, use glows on a
surface with room in every scheme, as in the next section.

## Dark-mode elevation

Glows are the mirror for dark-mode elevation and edge highlights: each level
lifts the surface a fixed number of tones. A glow resolves in all four
schemes, so it needs room above its surface in each of them. This ladder
sits on a light-gray surface, so the same tokens also lift cards toward white
in the light scheme.

A glow on a white light-mode surface has nothing to lighten in SDR, so Glaze
warns, even if you only use the token in dark mode. On a tone-100 surface it
leaves the glow transparent. A white written as `'#ffffff'` parses a hair
below tone 100, so its glow pins to opaque white instead, which paints the
same over it. Give glows a surface with room in every scheme, like the
tone-92 surface here.

## Tinted pigment

The pigment is pure black or pure white by default, so the surface keeps its
own hue under the overlay and the output stays neutral. A pigment tinted in
the surface's hue ([Josh Comeau's shadow advice](https://www.joshwcomeau.com/css/designing-shadows))
is opt-in through tuning:

* **`reach`** sets where the pigment sits: the fraction of the way from the
  surface's tone to black (shadow) or white (glow). `1` is pure black or
  white, the most reach; lower values let a tint show at the cost of reach.
* **`tint`** is the pigment's saturation, in the surface's hue. It shows only
  with `reach` below 1, since pure black and white have no hue. A neutral
  surface has no hue to give, so there the tint takes the object's hue when
  `hueBleed` is above 0, and the pigment stays neutral otherwise.
* **`hueBleed`** pulls the pigment's hue from the surface's hue toward the
  object's (`fg`). It needs `fg`, a `tint`, and `reach` below 1 to show.
* **`objectInfluence`** is the `β` in [object influence](#object-influence).

```ts
theme.colors({
  'shadow-tinted': {
    type: 'shadow',
    bg: 'surface',
    intensity: 8,
    tuning: { tint: 0.3, reach: 0.85 },
  },
});

glaze.configure({
  shadowTuning: { objectInfluence: 0.4 },
  glowTuning: { objectInfluence: 0.6 },
});
```

`shadowTuning` and `glowTuning` set the tuning globally (or per theme or token
through the config override); a per-color `tuning` merges over them field by
field. Both take these `LightingTuning` fields; defaults are in
[Color definitions](/reference/color-definitions/#lightingtuning) and
[Configuration](/reference/configuration/#glazeconfig).

The step is exact in every column. On light and mid surfaces a tint barely
changes the composite; on dark surfaces it costs reach and opacity. On the
figure's tone-12 surface, black reaches about 12 tones and needs α 0.545 for
the 8-tone shadow; the `tint: 0.3, reach: 0.85` pigment reaches about 10 tones
and needs α 0.706.

## Unreachable steps

Every surface has limited room: a shadow cannot go below black, and a glow
cannot go above white. When the step is out of reach (a glow on white, a deep
shadow on near-black), Glaze pins the overlay to the pigment's strongest
composite (full opacity, or fully transparent when the pigment cannot move the
surface at all) and emits a deduplicated `console.warn`. A 6-tone glow on a
tone-100 `auto` surface logs:

```text
glaze: glow "surface-highlight" cannot reach a 6.0-tone step in light
  scheme (reached 0.0 at full strength).
Lower the intensity, move the surface away from white, or raise a
  tuned-down `reach`.
```

Every shadow and glow resolves in all four schemes on every resolve, whatever
`modes` you emit and wherever you use the token, so the warning fires even for
a scheme you never show. Not using a token in a scheme only keeps the pinned
value off the screen.

To fix it, lower `intensity`, or give the overlay a surface with room in
every scheme. A tuned-down `reach` also costs room; raise it back toward 1.
Otherwise, expect the warning.

### When Glaze moves the surface

Your tone windows place the surfaces of the light and dark schemes. Two things
move them further on their own:

* **High contrast** bypasses the windows, so a page that is white in light mode
  becomes black in dark high contrast. A tone-97 page sits at tone 3 there,
  which leaves room for less than 3 tones.
* **A manual [contrast level](/schemes/#manual-contrast-level)** widens the
  windows toward the full range, and the dark page nears black faster than an
  interpolated `intensity` fades. On the default windows, the dark page sits
  at tone 9.25 at level 0 and about 3.15 at level 50, so `intensity: [8, 0]`
  asks for 4 tones there.

No `intensity` fits every level and both high-contrast schemes, because one
high-contrast entry serves a white page in light and a black one in dark. So
in these passes the step still pins to the pigment's strongest composite, but
Glaze warns only when it is out of reach in light and dark alike: then the step
itself is too big for its surface. When the other scheme has room, the pin is
quiet. In the passes your windows place (`contrastLevel` `'auto'` or `0`, not
high contrast), every shortfall warns as above.

| Pass                                                                  | A step out of reach                                                           |
| --------------------------------------------------------------------- | ----------------------------------------------------------------------------- |
| `light` / `dark`, at `'auto'` or level `0`                            | Pins and warns.                                                               |
| `light` / `dark` at any other level, `lightContrast` / `darkContrast` | Pins; warns only when it is out of reach in both light and dark of that pair. |

At level `100` the `light` / `dark` passes are the high-contrast ones and pair
up the same way.

On a display that can show brighter than white, a glow can also go past white:
see [HDR glows](/hdr/).

## High contrast

`intensity` accepts a `[normal, highContrast]` pair, like other numeric
fields:

```ts
theme.colors({
  'card-shadow': {
    type: 'shadow',
    bg: 'surface',
    fg: 'card',
    intensity: [8, 14],
  },
});
```

With a manual contrast level, the pair is interpolated like every other pair.
See [high-contrast pairs and the contrast level](/reference/contrast/#manual-contrast-level).
A step the level or high contrast puts out of reach pins quietly unless it is
out of reach in light and dark alike; see
[When Glaze moves the surface](#when-glaze-moves-the-surface).

## Fixed opacity

For one fixed-alpha color with no opacity solve, such as a modal scrim, set
`opacity` on a regular color:

```ts
theme.colors({
  scrim: { tone: 0, saturation: 0, mode: 'static', opacity: 0.5 },
});
// scrim → 'oklch(0 0 0 / 0.5)' in every scheme
```

`opacity` is applied after the color resolves, so its tone still adapts like
any regular color. Without `mode: 'static'`, `tone: 0` is lifted by the light
tone window and inverted into a light overlay in dark mode. A fixed alpha
gives a different step on every surface (an 8% black layer darkens white by
about 6 tones and a tone-10 surface by about 1), which is what shadows and
glows exist to avoid. For a translucent color with a contrast floor, use a
[transparent mix](/mixes/#transparent-mix).

## Outside a theme

`glaze.shadow()` and `glaze.glow()` compute an overlay without a theme. `bg`
and `fg` accept any color value Glaze parses (hex, `rgb()`, `okhsl()`, value
objects, …):

```ts
const shade = glaze.shadow({ bg: '#ffffff', intensity: 6 });
// → { h: 0, s: 0, t: 0, alpha: 0.0814… }

glaze.format(shade, 'rgb');
// → 'rgb(0 0 0 / 0.0814)'

const lift = glaze.glow({ bg: '#1e1e22', fg: '#3a3a44', intensity: 4 });
```

Both accept an optional `tuning` that merges over the global `shadowTuning` /
`glowTuning`.

The full input table is in
[Utilities](/reference/utilities/#glazeshadowinput--glazeglowinput).

## Coming from 2.x

In Glaze 2, `intensity` went through a curve in OKHSL lightness, so the same
number darkened white, mid, and dark surfaces by very different amounts, and
`fg` was a contrast reference that gave a white card on a light page almost no
shadow. 3.0 makes `intensity` an exact tone step, makes `fg` the object, and
adds `glow`. Upgrade steps are in
[3.0 — shadows and glows](/migration/#30--shadows-and-glows).
