Gesso
Theming

Retune

Set your own brand color, type, corners, and stroke on one block of CSS.

A retune is one [data-theme] block that points --brand-* at the colors you pick, and it is the path most consumers take. Leave the block unlayered, because unlayered CSS beats the gesso.brand layer wherever the block sits in your imports, and never put a retune inside @layer gesso.brand. Surfaces, ink, and lines stay with the system.

[data-theme="acme"] {
  --brand-primary: oklch(0.546 0.245 262);
  --brand-primary-fg: var(--neutral-50);
}

Then set <html data-theme="acme">. Anything you leave out keeps the dry value, which is ink for both hues, a system sans-serif for the UI, system serifs for body and titles, and 8px corners.

Dark

.dark inverts the chrome in system.css. It also inverts the dry brand, because the default primary and secondary are ink. A chromatic --brand-primary or --brand-secondary set on [data-theme] is not inverted, so a retune adds its own dark values, the way the prefabs do.

[data-theme="acme"].dark {
  --brand-primary: oklch(0.707 0.165 254);
  --brand-primary-fg: var(--neutral-950);
}

Put .dark on the same element as data-theme, as in <html class="dark" data-theme="acme">. The ink invert in brand.css applies to the element that carries .dark, so a .dark descendant of the data-theme element resets a chromatic brand to ink.

Color

Point each brand token at a color value or a neutral step, and not at another --brand-*. The kit ships no hue ramps, so write your own oklch() values. The defaults are the dry values.

GroupTokensDry default
Primary--brand-primary, --brand-primary-fgneutral 900, neutral 50
Secondary--brand-secondary, --brand-secondary-fgneutral 700, neutral 50

Each hue has a solid and a foreground, and nothing else to set. The primary solid is bg-primary with text-primary-foreground, and the second hue is the support tone, bg-tone with text-tone-fg inside data-tone="support", because shadcn already uses secondary for the fill wash. The tint, fill, edge, and ink for each hue are derived from its solid, so a chip or a quiet fill follows your color with no extra token.

[data-theme="acme"] {
  --brand-secondary: oklch(0.769 0.188 70);
  --brand-secondary-fg: var(--neutral-900);
}

Status colors live in the system, and a look can point a slot at another color. Success, warning, and info are the green, yellow, and blue categorical hues, danger is the cat red through --destructive, and the tint a tone draws is its solid mixed into the sunken well, at 8% in light and 16% in dark.

StatusWhere it is set
Danger--destructive and --destructive-foreground, which a look can override
Success, warning, info--tone-base, --tone-solid, and --tone-fg in the [data-tone="success"], [data-tone="warning"], and [data-tone="info"] blocks

To retune success, warning, or info, write the block yourself, unlayered, and every component that reads the tone follows:

[data-tone="success"] {
  --tone-base: oklch(0.6 0.15 160);
  --tone-solid: var(--tone-base);
  --tone-fg: var(--neutral-50);
}

Info in the brand

Info is the categorical blue by default. To draw it in a brand hue instead, set data-info="primary" or data-info="support" next to data-theme on the root, or on any element whose subtree should follow. Every info Alert, toast, Badge, and Progress then takes that hue, with its foreground for a solid, and the tint, ink, fill, and edge derive from it as they do for any tone.

<html data-theme="acme" data-info="primary">

Pick this when status messages should feel like the product and not like a system notice. Keep the blue when a brand hue is close to danger or warning, so info does not read as either.

Category hues

A look may retune the warm category hues, --cat-red, --cat-orange, and --cat-yellow, on its [data-theme] block. Danger reads the red and warning the yellow, so the status, its tint and ink, and every chip, segment, and mark in that hue follow from one value. Washi does this:

[data-theme="washi"] {
  --cat-red: oklch(0.57 0.15 33);
  --cat-orange: oklch(0.68 0.15 58);
  --cat-yellow: oklch(0.86 0.16 92);
}

Keep each hue recognizable, so a red still reads as danger and an orange stays apart from the yellow and the red. Leave green, blue, purple, pink, and gray alone, so success and info read the same in every look. A new red should still carry a white label for danger, and a new yellow a dark one for warning, so look at both on a button and an alert in light and dark before you ship.

shadcn's bg-secondary and bg-accent are aliases for the fill wash (bg-fill) and do not carry a brand color. The utilities for each role and the alias table are on Color, and switching looks in the sidebar shows blossom or orange landing on the support tone.

primarysupportdangersuccesswarninginfo

Type

A retune sets three faces, and bindings read --brand-font-* directly.

TokenUtilityUse
--brand-font-uifont-sansButtons, inputs, and the shell
--brand-font-bodyfont-bodyRunning text
--brand-font-displayfont-displayTitles

font-mono is --sys-font-mono and has no brand token. Sizes and leading are a named scale (text-display, text-heading-1 to text-heading-6, text-body-lg, text-body-md, text-body-sm, and text-caption), and a look changes the faces and never the metrics. The kit names a face and does not load it, so load the font files in your app. Each brand item ships a fonts.css that does this for its faces. Typography specimens are on Typography.

Radius and stroke

--radius-scale-* is the catalog a look picks from: 2xs is 2px, xs is 4px, sm is 6px, md is 8px, lg is 12px, xl is 16px, and full is a pill. --brand-radius is the pick for boxes such as cards and inputs, and it sets the unsized rounded and rounded-md, while rounded-lg adds 4px to it. --brand-radius-control is the pick for buttons and is read as rounded-control. Chips pin the md step in their own markup, so a pill control or a large box radius does not change them, and rounded-full stays a circle. Dry, Washi, and Ramson use md for both picks, ICS uses xl for boxes and full for controls, and Vistula uses 2xs, 2px, for both. Specimens are on Radius.

Stroke follows one token. Set --brand-font-weight-ui and border-stroke derives from it, with 400 giving 1px and 600 giving 2px, and the value is clamped between --stroke-1 and --stroke-2, so 500 gives 1.5px. The same weight is available as the font-ui utility.

Shadows use one ink that does not invert, with shadow-sm, shadow-md, and shadow-lg as the three sizes, and Elevation shows them. Overlays fade in on poppers, scale in on centered dialogs, and slide in on sheets, and Motion covers the tokens.

Next

Pick colors on a live stage on Studio and copy the block it writes, or start from a whole look on Prefabs.

On this page