Colors
A neutral foundation, blue actions, and semantic roles for light and dark interfaces.
Use colors by their purpose. A product can change its primary color while keeping the same component structure and behavior.
Color ramps
Five primitive families, from 50 to 950. Hover or focus a shade to inspect its value. Click, Enter, or Space copies the color value. Gray is Migoy’s generated ramp, sharing the primary hue with restrained chroma.
Gray comes from the approved Migoy recipe; Blue, Green, Red, and Amber come from the installed Tailwind palette. Components continue to use semantic roles. Green and Amber are palette choices; success and warning roles have not yet been defined.
Gray definition
The gray ramp is generated from tokens/gray.recipe.json. Hue is read from the theme’s light-mode primary at build time. Lightness runs from 0.985 to 0.145; chroma peaks at 0.018 and tapers to zero at both ends.
t = (step − 50) / 900
L = 0.985 − 0.84 × [0.2t + 0.8(3t² − 2t³)]
C = 0.018 × sin(πt)^0.9
H = primary hue (264.376° at generation)11 shades generated; 0 require chroma reduction to fit sRGB.
The generator checks ascending shades, descending lightness, and sRGB gamut. If a future recipe produces an out-of-gamut shade, it reduces chroma while preserving lightness and hue. These parameters are design choices, not a contrast guarantee.
The theme defines --gray-50 through --gray-950, with matching Tailwind gray-* utilities. The installable theme includes these primitive values, aliases, and semantic references together. The swatches above read the same generated data. Changing a primitive variable updates CSS references to it; changing the recipe or primary hue requires regeneration:
pnpm palette:build
pnpm palette:testPalette generation also runs before development and production builds.
White and black
White and black are separate primitives, not extra gray stops. They remain constant across themes; semantic roles choose which one to use.
:root {
--white: oklch(1 0 0);
--black: oklch(0 0 0);
}Use bg-background for the theme-aware page surface. bg-white and bg-black reference the explicit primitives when an element needs that fixed color in both themes. Gray 50 and Gray 950 remain near-white and near-black, rather than changing the ramp endpoints.
Live semantic mapping
Neutral roles now reference the generated gray variables directly. The value shown beside each swatch below is the actual reference; copying it preserves the relationship.
:root {
--background: var(--white);
--card: var(--gray-50);
--popover: var(--gray-50);
--muted: var(--gray-100);
--muted-foreground: var(--gray-600);
--border: var(--gray-200);
--input: var(--gray-400);
--foreground: var(--gray-950);
}
.dark {
--background: var(--black);
--card: var(--gray-800);
--muted: var(--gray-800);
--muted-foreground: var(--gray-400);
--foreground: var(--gray-50);
}Change a primitive at the theme scope and its semantic references and components follow through CSS. No component source needs regeneration. Changing the recipe or source primary requires a build to generate new primitives. Products adopt an updated theme explicitly; the registry does not push runtime changes into installed projects.
Page backgrounds use the explicit white and black primitives: --background: var(--white) in light mode and var(--black) in dark mode. Light cards and popovers keep Gray 50 with Gray 950 text; dark cards and popovers keep Gray 800 with Gray 50 text. Blue action fills, red destructive fills, link colors, and focus colors retain their current values. Neutral surfaces, text, borders, chart shades, and interaction states now use the gray ramp. Dark secondary actions use white labels to keep contrast when pressed; dark sidebar primary labels use Gray 950 against the existing brighter blue fill.
Surfaces and text
Use background and foreground for the page, card for grouped content, and popover for floating surfaces. Pair each surface with its matching foreground. Use muted for subdued surfaces and muted-foreground for supporting text.
backgroundvar(--white)var(--black)foregroundvar(--gray-950)var(--gray-50)cardvar(--gray-50)var(--gray-800)card-foregroundvar(--gray-950)var(--gray-50)popovervar(--gray-50)var(--gray-800)popover-foregroundvar(--gray-950)var(--gray-50)mutedvar(--gray-100)var(--gray-800)muted-foregroundvar(--gray-600)var(--gray-400)Actions and emphasis
Use primary for the main action, secondary for subdued filled actions, and accent for highlighted items. Pair filled actions with their matching foreground. Reserve destructive for actions that remove or destroy data. Use link for readable link text, particularly in dark mode.
primaryoklch(0.488 0.243 264.376)oklch(0.424 0.199 265.638)primary-foregroundoklch(0.97 0.014 254.604)oklch(0.97 0.014 254.604)secondaryvar(--gray-100)var(--gray-700)secondary-foregroundvar(--gray-950)whiteaccentvar(--gray-100)var(--gray-700)accent-foregroundvar(--gray-950)var(--gray-50)destructiveoklch(0.577 0.245 27.325)oklch(0.704 0.191 22.216)destructive-foregroundwhitevar(--background)linkvar(--primary)#93c5fdBorders and focus
border separates surfaces and is intentionally subtle. input is the border token for Input, Textarea, Input Group, and Combobox editing surfaces. control-border uses Gray 300 in both themes for outline buttons and other compact controls, independently of the text-entry border.
These trial shades fall below our 3:1 control-border target on the supported surfaces; see the contrast results below. Button uses focus-indicator for its keyboard outline. ring remains the general ring token; it is distinct from the Button focus indicator.
bordervar(--gray-200)var(--gray-700)inputvar(--gray-400)var(--gray-600)ringvar(--gray-500)var(--gray-400)control-bordervar(--gray-300)var(--gray-300)focus-indicatorvar(--primary)#93c5fdInteraction states
Neutral hover and pressed colors reference named gray steps. Primary and destructive states keep their existing color-mix formulas. Updating a referenced primitive updates the affected state. Disabled buttons use opacity rather than a separate color token.
primary-hovercolor-mix(in oklch, var(--primary), black 10%)color-mix(in oklch, var(--primary), white 10%)primary-pressedcolor-mix(in oklch, var(--primary), black 20%)color-mix(in oklch, var(--primary), white 15%)secondary-hovervar(--gray-200)var(--gray-600)secondary-pressedvar(--gray-300)var(--gray-500)accent-hovervar(--gray-100)var(--gray-700)accent-pressedvar(--gray-200)var(--gray-600)destructive-hovercolor-mix(in oklch, var(--destructive), black 10%)color-mix(in oklch, var(--destructive), white 10%)destructive-pressedcolor-mix(in oklch, var(--destructive), black 20%)color-mix(in oklch, var(--destructive), white 15%)See Button interaction states for the colors applied to real controls.
Sidebar
These roles belong to product navigation. They allow a sidebar to have its own surface, selected items, and focus treatment. The documentation navigation uses Fumadocs' separate theme.
sidebarvar(--gray-50)var(--gray-800)sidebar-foregroundvar(--gray-950)var(--gray-50)sidebar-primaryoklch(0.546 0.245 262.881)oklch(0.623 0.214 259.815)sidebar-primary-foregroundvar(--gray-50)var(--gray-950)sidebar-accentvar(--gray-100)var(--gray-700)sidebar-accent-foregroundvar(--gray-950)var(--gray-50)sidebar-bordervar(--gray-200)var(--gray-700)sidebar-ringvar(--gray-500)var(--gray-400)Charts
The current chart palette is monochrome. Pair series with labels, shapes, or line patterns; do not rely on these shades alone to distinguish data. Chart contrast and series distinguishability still need validation before use in production charts.
chart-1var(--gray-200)var(--gray-200)chart-2var(--gray-500)var(--gray-500)chart-3var(--gray-600)var(--gray-600)chart-4var(--gray-700)var(--gray-700)chart-5var(--gray-800)var(--gray-800)Contrast checks
| Theme | Lowest checked text pair | Lowest checked control pair |
|---|---|---|
| Light | 4.76:1destructive-foreground / destructive | 1.65:1Below targetcontrol-border / muted |
| Dark | 4.56:1secondary-foreground / secondary-pressed | 2.18:1Below targetinput / card |
The current checks cover text on its matching surfaces, supporting text on common neutral surfaces, action labels across neutral states, and input borders and focus colors on page, card, muted, and sidebar surfaces. Text pairs are checked at 4.5:1 and control/focus pairs at 3:1 using unrounded ratios. See text contrast and non-text contrast.
These are token-pair checks using sRGB luminance, not a complete accessibility audit. They exclude disabled opacity, decorative separators, arbitrary token combinations, and chart series. Existing blue/red colors outside sRGB are bounded to sRGB for this calculation; rendered components still need visual review. The chart palette still needs its own validation.
Using tokens
Prefer semantic utilities over literal values:
<section className="bg-card text-card-foreground border border-border">
<h2>Project settings</h2>
<p className="text-muted-foreground">Manage your project preferences.</p>
</section>In CSS, reference the variable rather than copying a fixed color:
.project-surface {
background: var(--card);
color: var(--card-foreground);
border: 1px solid var(--border);
}Semantic values appear beside their swatches. Use the copy buttons to copy light and dark CSS declarations. Values are read directly from the registry theme, including inherited dark values. Transparent colors are shown over their corresponding page background; interaction formulas resolve within each theme.
The theme primarily uses OKLCH, with derived color-mix() values and a few explicit colors for control contrast. Never communicate status using color alone. Check contrast in the actual component and state; this page is a token reference, not a guarantee that every token pairing is accessible.
To adopt the palette in a product, follow the optional theme installation. Components can also use your product's existing theme.