Colors
raft-ui colors come in three layers. Components never name a raw color: they ask for a purpose, such as “the fill of a primary button” or “error text”, and each theme decides which color that is.
| Layer | Name | Holds |
|---|---|---|
| Palette | --r-palette-{hue}-{1..12} | The Raft brand hues as 12-step scales |
| Alias | --rw-alias-{property}-{state}-{role} | What a color is for; points at one palette step |
| Component | --rl-{component}-* | A component’s own slots; read only aliases |
Palette
Each hue has 12 steps, and every step has the same job in every hue. Step 9 is the brand color itself. The light scales are shared by Brutal and Elegant light; Elegant dark swaps in dark scales with the same step meanings, so every alias follows the mode by itself. Switch the theme at the top of the page to see them.
contrast is the text color for a step-9 fill: a dark shade of the same hue, never plain black.
What each step is for
This follows Radix Colors.
| Step | Use | Alias |
|---|---|---|
| 1–2 | Page and subtle backgrounds | bg-page, bg-panel (neutral) |
| 3 / 4 / 5 | Component background: rest, hover, pressed | surface, surface-hover, surface-pressed |
| 6 / 7 / 8 | Border: subtle, default, hover | border-subtle, border, border-hover |
| 9 / 10 | Solid fill: rest, hover | fill, fill-hover |
| 11 | Colored text on white or on steps 1–5 | text |
| 12 | High-contrast text in the same hue | text-strong |
| contrast | Text on a solid fill | text-on-fill |
Two rules keep text readable:
- Text on a color is a dark shade of that color. A yellow fill takes dark olive-brown text, a cyan fill takes dark teal. Black or grey text on a colored background looks dirty.
- Colored text on a light background uses step 11. It is the most colorful shade that still passes WCAG AA (4.5:1) on steps 1–5.
Aliases
Names follow how-to-style: property first, then state, then the role last, so a component maps every type by changing only the last segment.
.r-button {
&&--error-type {
--rl-button-family-fill: var(--rw-alias-fill-error);
--rl-button-family-fill-hover: var(--rw-alias-fill-hover-error);
--rl-button-family-text: var(--rw-alias-text-error);
}
}The roles and the hue each one uses:
| surface3 | surface-hover4 | surface-pressed5 | border-subtle6 | border7 | border-hover8 | fill9 | fill-hover10 | text11 | text-strong12 | text-on-fillcontrast | |
|---|---|---|---|---|---|---|---|---|---|---|---|
| neutralgray | |||||||||||
| primaryyellow | |||||||||||
| accentpink | |||||||||||
| infocyan | |||||||||||
| successlime | |||||||||||
| warningorange | |||||||||||
| errorred |
Neutral text and backgrounds have their own names: --rw-alias-bg-page, --rw-alias-bg-panel, --rw-alias-bg-raised (something lifted above a recessed surface, like the selected segment: white in light mode, lighter than the surface in dark mode), --rw-alias-label-primary / -secondary / -tertiary / -disabled, --rw-alias-border-strong (Brutal’s ink) and --rw-alias-border-focus. --rw-static-white and --rw-static-black never change.
Feedback
A status (info, success, warning, error) has to read the same way on every component, so components don’t pick its colors: they read --rw-feedback-{property}-{role}, and each theme decides once how a status looks.
Status colors are their own, not the brand hues the roles above borrow. Error is a raspberry at 15°, warning an amber at 72° and success a true green at 153°, so the three are easy to tell apart and stay out of the warm brand band. Only error is loud; warning and success are quieter. Fields take the mid-tone; text takes a darker shade of the same hue so it passes AA (4.5:1). Dark mode has its own lighter set, where one color does both.
| Token | Elegant (light and dark) | Brutal |
|---|---|---|
--rw-feedback-text-{role} | the status color, darkened to pass AA in light mode; for messages and icons | same |
--rw-feedback-border-{role} | the status color | the status color, replacing the ink |
--rw-feedback-shadow-{role} | none | the hard shadow, in the status color |
A field in a status changes color as one piece: Elegant’s hairline takes the status color, and Brutal’s outline and hard shadow both do. Switch the theme at the top of the page to compare.
Input and Select’s status read these tokens; so will every field and message after them.
Contrast
Measured live for the current theme. Every pair a component uses should pass 4.5:1.
| pair | neutral | primary | accent | info | success | warning | error |
|---|---|---|---|---|---|---|---|
| 11 on panel | Aa— | Aa— | Aa— | Aa— | Aa— | Aa— | Aa— |
| 11 on 3 | Aa— | Aa— | Aa— | Aa— | Aa— | Aa— | Aa— |
| 11 on 5 | Aa— | Aa— | Aa— | Aa— | Aa— | Aa— | Aa— |
| contrast on 9 | Aa— | Aa— | Aa— | Aa— | Aa— | Aa— | Aa— |
| contrast on 10 | Aa— | Aa— | Aa— | Aa— | Aa— | Aa— | Aa— |
Changing colors
Override a palette step to recolor every alias that points at it, in the whole page or inside one ConfigProvider:
<ConfigProvider themeOverrides={{ "--r-palette-yellow-9": "oklch(0.86 0.17 85)" }}>…</ConfigProvider>Override an alias to change what one purpose uses without touching the palette:
<ConfigProvider themeOverrides={{ "--rw-alias-fill-primary": "var(--r-palette-orange-9)" }}>…</ConfigProvider>