Theming
Pick one color. Graphite builds the whole palette from it: every background, text color, border, button and status color, in light and dark, with each text pairing checked so what sits on it stays readable. You choose what a color is for, and the system decides what it is.
How color works
One color goes in. The engine turns it into named colors that each have a job, then wires those into the components.
- Your colorOne hex. The engine reads three things from it: which color it is, how intense, and how light.
- RampsEight strips of ten shades each. Four follow your color, four carry status.
- RolesThirty-two named jobs per theme, such as page background, body text and button fill. Each takes a shade and is checked for readability.
- StatesHover, pressed, selected, disabled and focus for the primary, secondary and danger families, as steps on their own ramps.
- Wired into componentsWritten as --graphite-* variables, so a new source repaints everything.
Why the math matters
The calculations run in OKLab, a color model built to match how eyes work: equal steps in its numbers look like equal steps to a person, so tone 30 to 40 reads as the same jump as 80 to 90. The models behind HSL and hex do not behave that way, which is why hand-picked palettes bunch up in the middle and flatten at the ends.
The ramps
A ramp is one color laid out from dark to light. Three follow your hue exactly at different intensities; secondary turns it 120° round the wheel, so the counterpoint is generated rather than picked. The two neutrals keep a trace of your color, which is what makes the finished interface read as one family.
| Ramp | Intensity | What it is for |
|---|---|---|
accent | Same as your color | Brand color, interactive elements, focus |
secondary | Just over half, 120° round the wheel | The counterpoint: the vivid tenth of 60/30/10 |
neutralVariant | Barely tinted | Borders and supporting text |
neutral | Almost gray | Page backgrounds, surfaces, primary text |
danger / warning / success / info | Tracks your color, within limits | Status and feedback. Hue is fixed per status |
Ten stops are shown per ramp, named 900 (darkest) to 050 (lightest). Each name stands for a tone on the 0 to 100 scale: 900 is tone 10, 800 is 20, and so on to 100 at 90 and 050 at 98. The engine can produce any tone in between, and often does. Your exact color is kept: on the accent ramp it replaces the nearest step, at its own tone, and secondary moves the same step to match. The Color foundation shows every stop live.
Color roles
Every color in the interface has a job. There are thirty-two of them per theme: you pick the job, the system picks the value.
One naming rule explains most of the list: a name starting with on is what goes on top of something else. onSurface is the text color for anything on surface, and that pairing is measured and guaranteed, not suggested.
Surfaces and backgrounds
| Role | Purpose | Light | Dark |
|---|---|---|---|
surface | Default container: cards, panels, sheets | #f8f8fc | #121215 |
surfaceElevated | One step up from surface, for what floats: menus, popovers, the modal panel | #ffffff | #201f23 |
surfaceVariant | Secondary surface: fields, hover fills, selected rows | #dedcea | #2e2c37 |
background | The page itself | #f8f8fc | #121215 |
background and surface resolve to the same value in both themes. Layers are separated by borders and surface variants; surfaceElevated is the one step up, kept for what floats over the page.
Content: text and icons
| Role | Purpose | Light | Dark |
|---|---|---|---|
onSurface | Content on a default surface | #030305 | #dedde2 |
onSurfaceVariant | Secondary text and icons, supporting copy | #2e2c37 | #bdbcc9 |
onBackground | Primary text and icons on the page | #030305 | #dedde2 |
Text hierarchy is two levels, not three. Icons follow text: primary icons take onBackground or onSurface, secondary icons take onSurfaceVariant.
Borders
| Role | Purpose | Light | Dark |
|---|---|---|---|
outline | Borders and dividers | #63626d | #807e8b |
One border role covers every border. Interactive borders bind to primary instead. It is checked against surface at 3:1, so borders are guaranteed perceivable.
Primary and secondary actions
| Role | Purpose | Light | Dark |
|---|---|---|---|
primary | Primary buttons, links, interactive borders and icons | #4c2f93 | #beb1ff |
onPrimary | Content on a primary fill | #f8f7ff | #1a0044 |
primaryContainer | Low-emphasis accent fill: selected rows, tags | #ddd9ff | #340b74 |
onPrimaryContainer | Content on primaryContainer | #040015 | #ddd9ff |
secondary | The counterpoint accent: secondary buttons, the vivid tenth of 60/30/10 | #005542 | #7dd1b6 |
onSecondary | Content on a secondary fill | #e7fff6 | #001c14 |
secondaryContainer | Low-emphasis secondary fill | #9ef2d6 | #00372a |
onSecondaryContainer | Content on secondaryContainer | #000503 | #9ef2d6 |
The pairing rule is strict: onPrimary goes on primary, onPrimaryContainer on primaryContainer, and the same for secondary. Mixing them across containers breaks the contrast guarantee. One recorded exception: the kit’s Button labels its secondary and danger fills with onPrimary, to keep one label color across variants. The engine measures both pairs too.
Status and feedback
| Role | Purpose | Light | Dark |
|---|---|---|---|
danger | Errors, destructive actions, invalid input | #880c06 | #ffa192 |
onDanger | Content on a danger fill | #fff6f5 | #330000 |
dangerContainer | Low-emphasis danger fill: banners, rows | #ffd2ca | #5d0000 |
onDangerContainer | Content on dangerContainer | #0e0000 | #ffd2ca |
warning | Warnings, risky but permitted actions | #733300 | #ffa570 |
onWarning | Content on a warning fill | #fff7f2 | #280d00 |
warningContainer | Low-emphasis warning fill: banners, rows | #ffd4bc | #4c1f00 |
onWarningContainer | Content on warningContainer | #0a0100 | #ffd4bc |
success | Confirmation, completion, valid input | #00572f | #5adb91 |
onSuccess | Content on a success fill | #e9fff0 | #001c0c |
successContainer | Low-emphasis success fill: banners, rows | #7dfcb0 | #00381d |
onSuccessContainer | Content on successContainer | #000501 | #7dfcb0 |
info | Neutral information, tips, in-progress states | #00478a | #8fc1ff |
onInfo | Content on a info fill | #f4f9ff | #001632 |
infoContainer | Low-emphasis info fill: banners, rows | #c8e1ff | #002d5c |
onInfoContainer | Content on infoContainer | #00030e | #c8e1ff |
Hue is fixed per status, so red still reads as danger whatever the source is. Chroma tracks the source, so statuses carry the same intensity as everything else. Containers work like primaryContainer: a low-emphasis fill for banners, rows and tags.
What the roles do not cover
- Links. There is no separate link role. Links bind to
primaryand keep their underline, so color is never the only affordance. - The scrim. It is not a role but it is generated:
--graphite-scrimis a translucent version of the darkest neutral, so it tracks the source too. It has no on-color and no contrast pairing. - Shadows. There is no shadow scale. Depth is outline, surfaceVariant and, for what floats, surfaceElevated.
Color hierarchy
What makes one thing read as sitting on another. Here that comes from borders and tinted areas, not from shading.
Because background and surface are the same value, depth is not built by stacking lighter or darker planes. It comes from containment (outline), emphasis (surfaceVariant) and attention (the accent ramp).
| Level | Role | Reads as |
|---|---|---|
| Page ground | background | The canvas |
| Container | surface + outline | A defined region |
| Floating | surfaceElevated | Something over the page: a menu, a dialog |
| Distinct region | surfaceVariant | A field, a hovered or grouped area |
| Selected or tagged | primaryContainer | Accented but not actionable |
| Primary action | primary | The thing to click |
| Primary content | onBackground / onSurface | What to read first |
| Secondary content | onSurfaceVariant | Supporting detail |
Themes
Light and dark are built at the same time from the same color. The names stay the same in both; only the values change.
You do not maintain two palettes. onSurface means main text on a panel in both themes, and comes out near-black in light and near-white in dark. Content and ground swap ends of the ramp, and the accent inverts with them, so the relationships survive even though the values are opposite.
| Role | Light | Dark | Direction |
|---|---|---|---|
background | #f8f8fc | #121215 | Light ground to dark ground |
onBackground | #030305 | #dedde2 | Dark text to light text |
primary | #4c2f93 | #beb1ff | Dark accent to light accent |
onPrimary | #f8f7ff | #1a0044 | Light label to dark label |
outline | #63626d | #807e8b | Mid to slightly lighter mid |
Contrast levels
Themes generate at one of two targets, applied to the whole theme rather than per role: AA (4.5:1 text, 3:1 non-text) or AAA (7:1 text, 3:1 non-text). The site is generating at AA right now.
Interaction states
How a color changes on hover, press, selection and focus, and when it is switched off.
A hovered button keeps its color and moves a few steps along its own ramp, which keeps it recognizably the same button with a readable label. Steps go darker in light and lighter in dark, away from the page, so the button gains prominence instead of fading. Three families carry a full set (primary, secondary, danger); primary is shown live below.
| State | Derivation | Light | Dark |
|---|---|---|---|
| Default | The role itself | #4c2f93 | #beb1ff |
| Hover | 6 tone steps | #3d1b80 | #d0c9ff |
| Pressed | 12 tone steps | #2f016d | #e4e0ff |
| Selected | 6 tone steps | #3d1b80 | #d0c9ff |
| Disabled | Neutral ramp | #dedde2 | #2e2d31 |
| Disabled content | Neutral ramp | #47474b | #808084 |
| Focus | A ring on the same ramp | #664db4 | #a08af7 |
- Hover and selected share a tone. Selection is told apart by persistence and a second affordance (a check, a weight, a border), not by color alone.
- Pressed is twice the hover step, so a press reads as its own event rather than a stronger hover.
- Disabled leaves the family’s ramp. Fill and content drop to neutral, so disabled reads the same whichever family it belongs to. It is low contrast on purpose, exempt under WCAG, and never the only sign a control is unavailable.
- Focus is a ring, not a fill. It stacks with hover, pressed and selected, so a focused control still shows its state.
Accessibility
Text has to stand out from what is behind it. The engine checks this before it hands over a palette, rather than leaving it to a test afterwards.
Eighteen pairings are measured in both themes every time a palette is generated. If one fell short, the engine would move that color along its own ramp until it passed, keeping the hue. The Accessibility page reruns a sweep of that at build time.
| Pairing | Target | Light | Dark |
|---|---|---|---|
onDanger on danger | 4.5:1 | 9.4:1Pass | 9.4:1Pass |
onDangerContainer on dangerContainer | 4.5:1 | 15.0:1Pass | 10.5:1Pass |
onWarning on warning | 4.5:1 | 9.0:1Pass | 9.5:1Pass |
onWarningContainer on warningContainer | 4.5:1 | 15.1:1Pass | 10.2:1Pass |
onSuccess on success | 4.5:1 | 8.3:1Pass | 10.2:1Pass |
onSuccessContainer on successContainer | 4.5:1 | 16.1:1Pass | 10.4:1Pass |
onInfo on info | 4.5:1 | 8.7:1Pass | 9.7:1Pass |
onInfoContainer on infoContainer | 4.5:1 | 15.4:1Pass | 10.2:1Pass |
onPrimary on primary | 4.5:1 | 9.2:1Pass | 9.7:1Pass |
onPrimaryContainer on primaryContainer | 4.5:1 | 15.2:1Pass | 10.7:1Pass |
onSecondary on secondary | 4.5:1 | 8.4:1Pass | 9.9:1Pass |
onSecondaryContainer on secondaryContainer | 4.5:1 | 15.8:1Pass | 10.2:1Pass |
onPrimary on secondary | 4.5:1 | 8.3:1Pass | 10.3:1Pass |
onPrimary on danger | 4.5:1 | 9.4:1Pass | 9.5:1Pass |
onSurface on surface | 4.5:1 | 19.5:1Pass | 13.8:1Pass |
onSurfaceVariant on surfaceVariant | 4.5:1 | 10.1:1Pass | 7.3:1Pass |
onBackground on background | 4.5:1 | 19.5:1Pass | 13.8:1Pass |
outline on surface | 3:1 (UI) | 5.7:1Pass | 4.7:1Pass |
What is not guaranteed
onSurfaceVariantonsurfaceis not a checked pairing. It is common and usually fine, but verify it.- Any cross-pairing you invent:
onPrimaryContaineronsurface,primaryas body text,outlineas text. - Text over images, gradients or generative art. Put a solid surface behind it.
- Disabled states, which are exempt by design.
Usage
Six rules each way. Most of them are the same rule: build with the job, not the value.
Do
- Assign roles, not values. Reach for primary or onSurfaceVariant, never the hex they resolve to today.
- Respect on pairings. onSurface belongs on surface; onPrimary belongs on primary.
- Use outline and surfaceVariant for depth, and surfaceElevated only for what floats.
- Check the contrast table when you change the source color, especially at AAA.
- Pair color with a second signal for any state or status, because a status hue can collide with the source.
- Design in both themes before handing off.
Don’t
- Don’t apply raw hex values to components. A hex is one source color in one theme.
- Don’t reference primitives directly. accent 40 is a color without a job.
- Don’t invent status colors from the accent or neutral ramps. Use the status roles.
- Don’t use primaryContainer as a general surface: it makes everything look selected.
- Don’t nest three or more surface levels. There is no third value to resolve to.
- Don’t build hover or selected states with opacity. States are tone steps on the ramp.
Tokens
How the roles are named in code, and how to pick the right one.
Naming
Roles are camelCase in the engine and JSON (onSurfaceVariant) and kebab-case CSS variables on the site (--graphite-on-surface-variant). The CSS that Create exports uses the same names. The Tokens foundation lists every variable live.
Choosing a role
Work down this order and stop at the first match.
- What is the element? A ground, a container, content, a border, or an action.
- Ground or container? background for the page, surface for a container, surfaceElevated for something floating, surfaceVariant for a field or distinct region.
- Content? The on- role matching whatever it sits on.
- Border? outline, or primary and the focus ring if it marks interaction.
- Action? primary with onPrimary for full emphasis, secondary for the counterpoint, a container pair for low emphasis.
- Status? danger, warning, success or info, with their containers for quiet fills.
- Interactive state? The state variable for that family, never a hand-adjusted value.
- No match? The role is missing. Flag it rather than working around it.
Complete reference
Roles: 32 per theme
--graphite-primary--graphite-on-primary--graphite-primary-container--graphite-on-primary-container--graphite-secondary--graphite-on-secondary--graphite-secondary-container--graphite-on-secondary-container--graphite-surface--graphite-surface-elevated--graphite-on-surface--graphite-surface-variant--graphite-on-surface-variant--graphite-outline--graphite-background--graphite-on-background--graphite-danger--graphite-on-danger--graphite-danger-container--graphite-on-danger-container--graphite-warning--graphite-on-warning--graphite-warning-container--graphite-on-warning-container--graphite-success--graphite-on-success--graphite-success-container--graphite-on-success-container--graphite-info--graphite-on-info--graphite-info-container--graphite-on-info-container
Interaction states: 19 per theme
--graphite-primary-hover--graphite-primary-pressed--graphite-primary-selected--graphite-primary-disabled--graphite-primary-disabled-content--graphite-primary-focus--graphite-secondary-hover--graphite-secondary-pressed--graphite-secondary-selected--graphite-secondary-disabled--graphite-secondary-disabled-content--graphite-secondary-focus--graphite-danger-hover--graphite-danger-pressed--graphite-danger-selected--graphite-danger-disabled--graphite-danger-disabled-content--graphite-danger-focus--graphite-focus
Primitives: 8 ramps × 10 stops
For reference and tooling. Inspect and copy them; do not design with them.
In Figma
The kit carries the same system as variables: semantic roles with a Light and a Dark mode, and the primitives as their own collection. Pick roles in a design, never primitive stops, and regenerate rather than hand-edit when the source changes.
Glossary
Plain definitions for ramp, role, tone and the rest.
Every term on this page is defined in the glossary.
Next steps
See the palette, build one, or check it holds up.
- Create
Pick a source, tune the theme against a live preview, and take the code.
- Color
All eight ramps at every stop, live from the header's source.
- Accessibility
The contrast targets, the sweep behind them, and the gaps that are still open.
