Get started
Fabricator UI

Search documentation

Search pages, components, blocks, styles and colours.

Surfaces

PreviousNext

Eight elevation levels, each with its own shadow, so layers stay distinct in light and dark mode.

Installation

The surface tokens ship with the foundations:

bunx --bun fabricator-ui@latest add foundations

The ladder

Each level pairs a background with a shadow: bg-surface-N shadow-surface-N. In light mode, levels 1 and 2 are off-white and everything from level 3 up is white (tinted, when you set a base colour): the shadow alone carries the elevation. In dark mode, each level is a step lighter than the one below, and the shadow adds a faint top highlight and ring.

1
Page
2
Sidebar
3
Card, popover
4
Active tab
5
Dialog
6
Above a dialog
7
Menu in a dialog
8
Top
Switch the theme to compare. Neutral light flattens to white from level 3 and lets the shadow carry the elevation; dark lifts each level a step lighter. With a base colour, every level in both themes carries the tint.

Surfaces have no borders. Every surface shadow starts with a 1px ring, which draws the edge.

Base colour

Every surface derives from one base colour, held by two variables: --surface-hue (an OKLCH hue in degrees) and --surface-chroma (how strongly the surfaces lean towards it, from 0 to 0.04). Both are 0 by default, which gives the neutral ladder above. Set them here to tint this whole site:

Hue
0°
Strength
0%
LightThreadsAgentsSettings
OverviewLogs
Runs this week128 runs, 4 failed
Rerun failed?Four jobs will run again.Rerun
1
2
3
4
5
6
7
8
DarkThreadsAgentsSettings
OverviewLogs
Runs this week128 runs, 4 failed
Rerun failed?Four jobs will run again.Rerun
1
2
3
4
5
6
7
8
Start from a preset, or set your own: hue sets --surface-hue and strength sets --surface-chroma (100% is 0.04). Text keeps AA contrast at every strength. The choice applies to the whole site, like the Surface row in the settings menu, and is saved in this browser.
globals.css (neutral: these are the defaults)
:root {
  --surface-hue: 0;
  --surface-chroma: 0;
}

/* Let the theme tokens follow the surfaces. */

:root {
  --background: var(--surface-1);
  --card: var(--surface-3);
  --popover: var(--surface-3);
  --sidebar: var(--surface-2);
  --muted: oklch(calc(0.967 - var(--surface-chroma) * 0.5) calc(var(--surface-chroma) * 0.4) var(--surface-hue));
  --muted-foreground: oklch(calc(0.556 - var(--surface-chroma) * 0.4) 0 0);
}

.dark {
  --background: var(--surface-1);
  --card: var(--surface-3);
  --popover: var(--surface-3);
  --sidebar: var(--surface-2);
  --muted: oklch(0.235 calc(var(--surface-chroma) * 0.85) var(--surface-hue));
}

Each level takes a share of the chroma:

  • Light mode tints every level, like dark mode: the page takes 44% of the chroma (about oklch(0.962 0.018) at full strength) and the top level 21.5%. Colour needs room below white, so as the tint grows the ladder settles into a band just below white (0.962 to 0.981 at full strength), still lighter level by level, and muted-foreground darkens with it to stay AA on the page.
  • Dark mode carries the tint: level 1 takes 90% of the chroma, and each level above takes a little less, so the top of the ladder stays calm.

At full strength, every level stays inside sRGB for any hue. Text stays AA wherever it was AA on the neutral ladder. The opaque bg-selected and bg-muted fills follow the tint too.

/* light */
--surface-1: oklch(
  calc(0.985 - var(--surface-chroma) * 0.575) calc(var(--surface-chroma) * 0.44)
    var(--surface-hue)
);
/* dark */
--surface-1: oklch(0.205 calc(var(--surface-chroma) * 0.9) var(--surface-hue));

The fabricator preset writes background, card, popover, sidebar, muted and muted-foreground as plain colours, because the CLI only maps literal colours to Tailwind utilities. To have them follow the tint, point them at the surfaces, as the second part of the generated CSS does. Tailwind keeps the utilities the preset already mapped, so bg-background and bg-card keep working.

Where each level goes

WorkspaceThreadsAgents
OverviewLogs
Runs this week128 runs, 4 failed
Rerun failed jobs?Four jobs will run again.
Page (1), sidebar (2), card (3), active tab (4) and a dialog (5) over it. No borders: every edge is the shadow's 1px ring.
ElementClasses
Pagebg-background (surface 1)
Sidebarbg-sidebar (surface 2); an inset main area is bg-surface-2 shadow-surface-2
Raised cardbg-surface-3 shadow-surface-3
Popover, dropdown, select, combobox, context menu, hover cardbg-surface-3 shadow-surface-3 rounded-xl
Active tabbg-surface-4 shadow-surface-4
Dialog, alert dialogbg-surface-5 shadow-surface-5 rounded-xl
Sheet, drawerbg-surface-3 shadow-surface-3
TooltipInverted: bg-foreground text-background

Overlays behind dialogs and sheets are bg-black/40, and bg-black/80 in dark mode.

Interaction overlays

Hover and press states are translucent overlays, so they work on any surface.

bg-hoverLit rows, ghost hover
bg-activePressed, selected
bg-selectedSolid selection
bg-tintSecondary fill
bg-destructive-lightDestructive fill
TokenLightDarkUse
bg-hover4% black6% whiteHover, the Fluid Hover highlight
bg-active7% black10% whitePressed, selected rows, toggled on
bg-selectedSolid greySolid greySelection that must stay opaque
bg-tint, bg-tint-hover8%, 6.5% black25%, 20% whiteSecondary buttons and fills
bg-destructive-lightPale redDeep redDestructive buttons and fills

Status colours

Info, success, warning and error each have a full palette, so any component can carry a status: alerts, badges, toasts, callouts or your own. Every status has the same eight-level ladder as the neutral surfaces, tinted, and works with the same shadows, so you can raise parts inside a status block exactly like on the page.

Info
1
2
3
4
5
6
7
8
Success
1
2
3
4
5
6
7
8
Warning
1
2
3
4
5
6
7
8
Error
1
2
3
4
5
6
7
8
Each status has the same eight levels as the neutral surfaces, tinted, and pairs with the same shadows. Switch the theme to compare.

Each status also has a solid colour for icons and fills, a foreground for text on that solid, a text colour for copy on its surfaces (and on the neutral ones), and a hairline border:

Aa
Info text on its surfacesolid · foreground · text · border
Aa
Success text on its surfacesolid · foreground · text · border
Aa
Warning text on its surfacesolid · foreground · text · border
Aa
Error text on its surfacesolid · foreground · text · border
TokenUtilityUse
--<status>-surface-1 … 8bg-<status>-surface-1 … 8Tinted surfaces; pair with shadow-surface-N
--<status>bg-<status>, text-<status>, fill-…Icons, dots, solid fills
--<status>-foregroundtext-<status>-foregroundText on a solid bg-<status>
--<status>-texttext-<status>-textText on status and neutral surfaces
--<status>-borderborder-<status>-border, ring-…Hairlines

<status> is info, success, warning or error. Every pair is checked for WCAG AA in light and dark: text at 4.5:1 and the solid at 3:1 on all sixteen surfaces, and the foreground at 4.5:1 on the solid.

<div className="rounded-xl bg-success-surface-3 p-4 text-success-text shadow-surface-3">
  Deployed to production.
</div>

Use the low-contrast form (a neutral surface with a status-coloured icon) for most messages, and the tinted surfaces when the whole block should read as the status. Alert offers both through its contrast prop.

Focus

Focus is a 1px blue ring with no offset, the same in both themes: focus-visible:ring-1 focus-visible:ring-ring. The colour is also available as --focus-ring.

Tokens

VariableUtility
--surface-hue, --surface-chromaThe base colour (see Base colour)
--surface-1 … 8bg-surface-1 … bg-surface-8
--shadow-1 … 8shadow-surface-1 … shadow-surface-8
--hoverbg-hover
--activebg-active
--selectedbg-selected
--tintbg-tint
--tint-hoverbg-tint-hover
--destructive-lightbg-destructive-light
--focus-ringring-focus-ring
--info, --success, --warning, --error and their palettesSee Status colours

These are new names: the theme tokens such as background, card and border keep their meaning, so components from other registries still fit in.