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 · default(owned bycva)color—brand · gray · 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/ui-react/button';
<Button variant="filled" color="brand">Primary</Button>
<Button variant="light" color="red">Destructive</Button>
<Button variant="subtle" color="gray">Quiet</Button>Palette
Eleven colors, two roles. brand is the default accent and gray is the neutral accent. The other nine carry semantic weight by convention (red for destructive, green for success, yellow for warning, blue for info) but the kit doesn't lock you in — pick whatever reads right for the surface.
data-color="brand"data-color="gray"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"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
Want to see each slot? Here is every palette laid out across all six.
brandgrayredorangeyellowgreentealcyanbluepurplepinkToggle 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 seventeen named scales (plus white and black): five gray flavors and twelve accents. Only the eleven listed in COLORS have a data-color block today; the rest (gray-modern, gray-blue, gray-true, lime, rose) are wired to Tailwind and ready to be promoted — or used directly via bg-rose-500, text-gray-modern-700, etc.
coregray-lightgray-darkgray-moderngray-bluegray-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.
brandgrayredorangeyellowgreentealcyanbluepurplepinkThe same eleven palettes drop straight into ActionIcon without a second resolver — data-color is component-agnostic.
brandgrayredorangeyellowgreentealcyanbluepurplepinkSemantic intent through color
There is no destructive, success, or warning variant. Intent rides on color over the right shape — five variants × eleven palettes covers the space without a combinatorial taxonomy.
Destructive · filled · red
Success · filled · green
Warning · light · yellow
Info · light · blue
Quiet · subtle · gray
Marketing · filled · purple
The default variant
Sometimes you want a neutral chrome — a toolbar trigger, a quiet card action — that doesn't shout a palette. default is the escape hatch: it draws on neutral gray-light / gray-dark tokens for fill and text, ignoring color there. The focus ring still picks up --c-solid, so keyboard users see the accent on the way through.
Tab through them to see the ring shift while the fill stays neutral.
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/ui-react/cn';
import { type Color, DEFAULT_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)]',
default: '/* palette-independent — neutral surface */',
},
},
});
export const Foo = ({ color = DEFAULT_COLOR, variant, ...rest }: Props) => (
<ark.button data-color={color} className={cn(variants({ variant }))} {...rest} />
);Three rules to keep the abstraction honest:
- Always set
data-color— even onvariant="default". The focus ring still tints to the palette. - Never reach past the slots. Inside a colored variant,
bg-blue-500is a bug — it breaks dark mode and breaks consumer-defined palettes. defaultis palette-independent for fill and text. Use neutralgray-light/gray-darktokens; reserve--c-solidfor the focus ring.
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/ui-react';
COLORS;
// readonly ['brand', 'gray', '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/ui-react); component barrels only re-export their own types.