Colors
Eleven palettes × six slot variables — the orthogonal color system that powers every accented component in the kit.
Color in 42UI is orthogonal to variant. The same eleven palettes flow through every accented component, the same six CSS slots underneath. Pick the shape with variant and the palette with color — they compose without knowing about each other.
variant—filled · light · outline · subtle(owned bycva)color—brand · neutral · red · orange · yellow · green · teal · cyan · blue · purple · pink(owned bydata-coloron the root)
data-color is the entire interop surface. Resolve a palette at the CSS layer once, and every variant in every component picks it up — including dark mode and consumer-defined palettes.
import { Button } from '@42-central/ui-react/button';
<Button variant="filled" color="brand">Primary</Button>
<Button variant="light" color="red">Destructive</Button>
<Button variant="subtle" color="neutral">Quiet</Button>Palette
Eleven colors, a few roles. brand is the default accent and neutral is the colorless one: a black fill with white text in light theme, a white fill with navy text in dark, and translucent washes for light and hover. It is the kit's own chrome (Button's zero-config default) and the color the Figma kit labels "gray" on Button. It goes through the same --c-* slots as every other color. The other nine carry semantic weight by convention (red for destructive, green for success, yellow for warning, blue for InfoIcon) but the kit doesn't lock you in — pick whatever reads right for the component.
data-color="brand"data-color="neutral"data-color="red"data-color="orange"data-color="yellow"data-color="green"data-color="teal"data-color="cyan"data-color="blue"data-color="purple"data-color="pink"Migrating from gray
gray was removed: it duplicated neutral, the color the Figma kit already draws as "gray" on Button. color="gray" still renders, as neutral, until the next minor. Replace it with color="neutral". Expect filled to turn black/white and light to get a heavier wash; subtle, outline and text keep their color.
TypeScript can't flag it, since Color accepts any string. The package ships a Biome plugin that warns on color="gray" in JSX. Enable it in your biome.json:
{
"plugins": ["./node_modules/@42-central/ui-react/biome/no-gray-color.grit"]
}Slot variables
Every palette resolves to six CSS variables defined in colors.css. Components reference slots, not steps — bg-[var(--c-solid)] does the right thing whether the palette is brand or pink, in light or dark mode.
Prop
Type
Disabled slots
A disabled control should read as inactive whatever its palette, so the disabled slots are deliberately palette-independent — translucent neutral washes (black in light, white in dark) declared once on :root / [data-theme="dark"]. Being translucent rather than opaque gray, they take the tint of whatever surface they sit on. Every data-color inherits the same muted set, so they don't repeat per palette. Need a palette-specific disabled look? Override [data-color="x"] { --c-solid-disabled: … } in your own CSS.
Prop
Type
Want to see each slot? Here is every palette laid out across all six.
brandneutralredorangeyellowgreentealcyanbluepurplepinkToggle the docs theme to watch every slot rebalance — the dark map keeps fills bright and surfaces deep so contrast stays put.
Token scales
Every slot above resolves to a step on a deeper token scale — --color-<name>-{25..950}. The kit ships fifteen named scales (plus white and black): three gray flavors and twelve accents. The eleven palettes listed in COLORS each have a data-color block: ten sit on an accent scale, and neutral is composed from white, black and the gray scales. The rest (lime, rose, gray-true) are wired to Tailwind and ready to be promoted — or used directly via bg-rose-500, text-gray-true-700, etc.
coregray-lightgray-darkgray-truebrandredorangeyellowlimegreentealcyanbluepurplepinkroseStep 500 is the resting solid for most accents — 400 for yellow, where 500 reads too dim on white. Step 700 is the canonical text token on non-filled surfaces in light mode; 300 in dark. The slot map in colors.css formalises these picks per palette so components never reach past --c-*.
Variants × palette
Each variant binds a small subset of slots. Swap color, the rest follows.
brandneutralredorangeyellowgreentealcyanbluepurplepinkThe same eleven palettes drop straight into ActionIcon without a second resolver — data-color is component-agnostic.
brandneutralredorangeyellowgreentealcyanbluepurplepinkSemantic intent through color
There is no destructive, success, or warning variant. Intent rides on color over the right shape — four variants × eleven palettes covers the space without a combinatorial taxonomy.
Primary | filled · neutral
Secondary | light · neutral
Destructive | filled · red
Success | filled · green
Warning | light · yellow
Info | light · blue
Quiet | subtle · neutral
filled · purpleSurfaces
Distinct from the accent system above: Card (and bordered surfaces generally) aren't meant to pop with a color — they're meant to sit quietly on whatever the host app's page actually looks like. Their default, outline, and gradient variants don't read --c-solid at all. Instead of a fixed hue, they use a flat, low-opacity white wash — bg-white/4, the same value in both light and dark theme. A translucent tint always reads as "a little lighter than whatever's behind it"; a hardcoded opaque fill would look right on the kit's own dark-navy chrome but clash on a host app whose page happens to already be blue, or genuinely white, or anything else the kit doesn't control.
Text and border still adapt to the app's light/dark theme — navy text on a navy-tinted border in light theme, white text on a white-tinted border in dark theme. Only the fill itself stays theme-agnostic and background-dependent.
On the docs page itself
Same <Card padding="sm">…</Card>, two different backgrounds behind it — the card's own fill never changes, only what shows through it does.
filled and light are the exception: those two variants exist specifically to accept a color and do read the accent slots (--c-solid / --c-soft), same as Button. Reach for them when a surface needs to carry semantic color; reach for default / outline / gradient for a plain content surface that has to work on a page background the kit doesn't control.
Consuming the system in a new component
Every colored component follows the same shape. Bind a handful of slots in cva, then write data-color on the root.
import { cva } from "class-variance-authority";
import { ark } from "@ark-ui/react";
import { cn } from "@42-central/ui-react/cn";
import { type Color } from "../../lib/colors";
const variants = cva("...", {
variants: {
variant: {
filled:
"bg-[var(--c-solid)] text-[var(--c-on-solid)] hover:bg-[var(--c-solid-hover)]",
light:
"bg-[var(--c-soft)] text-[var(--c-text)] hover:bg-[var(--c-soft-hover)]",
outline:
"bg-transparent text-[var(--c-text)] border border-[var(--c-solid)] hover:bg-[var(--c-soft)]",
subtle: "bg-transparent text-[var(--c-text)] hover:bg-[var(--c-soft)]",
},
},
});
export const Foo = ({
color = "var(--primary-color)",
variant,
...rest
}: Props) => (
<ark.button
data-color={color}
className={cn(variants({ variant }))}
{...rest}
/>
);One rule to keep the abstraction honest:
- Never reach past the slots. Inside a colored variant,
bg-blue-500is a bug — it breaks dark mode and breaks consumer-defined palettes.
Adding a new color (3 steps)
None of these touch a component file.
- Define
--color-<name>-{25..950}intheme.css. - Append the name to
COLORSinlib/colors.ts. - Add a
[data-color="<name>"]block (and a[data-theme="dark"] [data-color="<name>"]override) tocolors.csswith the six slots.
/* colors.css */
[data-color="indigo"] {
--c-solid: var(--color-indigo-500);
--c-solid-hover: var(--color-indigo-600);
--c-on-solid: var(--color-white);
--c-soft: var(--color-indigo-50);
--c-soft-hover: var(--color-indigo-100);
--c-text: var(--color-indigo-700);
}
[data-theme="dark"] [data-color="indigo"] {
--c-solid: var(--color-indigo-500);
--c-solid-hover: var(--color-indigo-400);
--c-on-solid: var(--color-gray-dark-950);
--c-soft: var(--color-indigo-950);
--c-soft-hover: var(--color-indigo-900);
--c-text: var(--color-indigo-300);
}Custom palette without forking
Color is (typeof COLORS)[number] | (string & {}), so autocomplete still suggests the eleven built-ins while letting consumers ship their own data-color block in app CSS. Same six slots, same component code, no fork of the kit.
/* app's globals.css */
[data-color="lime"] {
--c-solid: #84cc16;
--c-solid-hover: #65a30d;
--c-on-solid: #0a0d12;
--c-soft: #ecfccb;
--c-soft-hover: #d9f99d;
--c-text: #4d7c0f;
}<Button color="lime">Custom palette</Button>API
import { COLORS, type Color } from "@42-central/ui-react";
COLORS;
// readonly ['brand', 'neutral', 'red', 'orange', 'yellow',
// 'green', 'teal', 'cyan', 'blue', 'purple', 'pink']
type Color = (typeof COLORS)[number] | (string & {});COLORS is exported at runtime for iteration — story controls, this docs page, anywhere you need to enumerate the built-in palettes. Color is the type to accept on any colored prop. Both live on the package root (@42-central/ui-react); component barrels only re-export their own types.
Changelog
- #289
The
graypalette is removed: useneutral, which the Figma kit already draws as "gray".color="gray"renders asneutraluntil the next minor, sofilledturns black/white andlightgets a heavier wash - #289
The unused
gray-modern-*andgray-blue-*scales are removed;gray-light,gray-darkandgray-truestay - #289
A Biome plugin,
biome/no-gray-color.grit, warns oncolor="gray"in JSX to help you migrate
- #80
Raise the contrast of neutral's
--c-soft
- #35
neutralcolor: a white fill with dark-navy text
Color system: one
variant×colormodel that every accent-aware component reads offdata-color