Skip to main contentGraphite UI

Docs

Color

Pick one color. Graphite UI builds the whole palette from it: every background, text color, border, button, and status color, in both light and dark, each pair checked to make sure the text on it is readable.

The important idea is this: you choose what a color is for, not what it is. You say “this is the page background” or “this is a button”, and the system decides the actual value. So when the source color changes, everything updates together and stays readable, and nobody has to go and edit components.

Every value on this page is live. It reflects the source color set in the header right now. Change it and these tables re-resolve.

How color works

In shortOne color goes in. The system turns it into a set of named colors that each have a job, then wires those into the components you build with.

It happens in five steps. Each one takes the result of the step above and adds a decision.

  1. 1Your colorOne color. The system reads three things from it: which color it is, how intense, and how light.
  2. 2Raw shadesSeven strips of ten shades each, dark to light. Three built from your color, four fixed ones for danger, warning, success, and info.
  3. 3Named colorsTwenty-seven jobs, such as page background, body text, and button fill. Each gets a shade, and each is checked for readability.
  4. 4StatesWhat each of those looks like on hover, on click, when selected, when switched off, and when focused.
  5. 5Wired into components59 variables the component library reads, so changing a color actually repaints the product.

Why the math matters

The calculations run in OKLab, a way of describing color built to match how eyes actually work. Its useful property: equal steps in the numbers look like equal steps to a person. Going from tone 30 to 40 looks like the same size jump as going from 80 to 90.

Older color models (the ones behind HSL and hex codes) do not behave that way. That is why hand-picked palettes so often bunch up in the middle and flatten out at the light and dark ends.

Step 2: the ramps

A rampis one color laid out from dark to light, like a paint strip. The system keeps your color’s hue and varies how light it is, then repeats that at three levels of intensity:

Ramp
Intensity
What it is for
AccentSame as your colorBrand color, interactive elements, focus
Neutral variantBarely tintedSecondary surfaces, borders, supporting text
NeutralAlmost grayPage backgrounds, primary surfaces, primary text
Error / Warning / Success / InfoTracks your color, within limitsStatus and feedback. Hue is fixed per status, not derived

The two neutrals are not quite gray. They keep a trace of your color, and that trace is what makes the finished interface look like one family instead of a brand color dropped onto a gray page. Each ramp gives you ten steps, labeled 900 (darkest) through 050 (lightest).

Two things worth knowing. The ten steps are just the ones shown. The system can produce any shade in between, and often does. Your exact color is kept. It appears on the accent ramp unchanged, taking the place of whichever step it sits closest to. Your brand color survives intact.

These raw colors have no meaning attached. Something like accent 40is just a shade. It is not “the button color.” Never reach for one directly in a design; use the named roles in the next section instead.

What you get out

Two files. CSS for engineers to point a build at, and JSON for tooling and design files. Alongside each color the export records where it came from, which ramp and which step, so “why is this color this color?” always has an answer.

Color roles

In shortEvery color in the interface has a job: page background, body text, button fill, error message. You pick the job; the system picks the value.

There are twenty-seven of these jobs, and each theme fills them in. 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 sitting on surface. That pairing is not a suggestion. The system measures it and guarantees it is readable.

The roles are grouped below by what they are for. You will use a handful of them constantly and most of the rest rarely.

Surfaces and backgrounds

Token
Purpose
Light
Dark
backgroundThe page itself#f8f8fcneutral 050#121215neutral 18
surfaceDefault container: cards, panels, sheets#f8f8fcneutral 050#121215neutral 18
surfaceVariantSecondary surface: fields, hover fills, selected rows, tags#dedceaneutralVariant 100#2e2c37neutralVariant 700

background and surface resolve to the same value in both themes. Layers are separated by borders and surface variants, not by shading.

Content: text and icons

Token
Purpose
Light
Dark
onBackgroundPrimary text and primary icons#030305neutral 900#dedde2neutral 100
onSurfaceContent on a default surface#030305neutral 900#dedde2neutral 100
onSurfaceVariantSecondary text and icons, supporting copy#2e2c37neutralVariant 700#bdbcc9neutralVariant 200

Text hierarchy is two levels, not three. Icons follow text: primary icons take onBackground, secondary icons take onSurfaceVariant.

Borders

Token
Purpose
Light
Dark
outlineAll borders and dividers#63626dneutralVariant 500#807e8bneutralVariant 400

One border token covers every border in the system. Interactive borders bind to primary instead. Checked against surface at 3:1, so borders are guaranteed perceivable.

Primary actions

Token
Purpose
Light
Dark
primaryPrimary buttons, links, interactive borders and icons#4c2f93accent 600#beb1ffaccent 200
onPrimaryContent on a primary fill#f8f7ffaccent 050#1a0044accent 800
primaryContainerLow-emphasis accent fill: selected rows, tags#ddd9ffaccent 100#340b74accent 700
onPrimaryContainerContent on primaryContainer#040015accent 900#ddd9ffaccent 100

The pairing rule is strict: onPrimary goes on primary, onPrimaryContainer goes on primaryContainer. Mixing them across containers breaks the contrast guarantee.

Status and feedback

Token
Purpose
Light
Dark
dangerErrors, destructive actions, invalid input#880c06danger 600#ffa192danger 200
onDangerContent on a danger fill#fff6f5danger 050#330000danger 800
dangerContainerLow-emphasis danger fill: banners, rows#ffd2cadanger 100#5d0000danger 700
onDangerContainerContent on dangerContainer#0e0000danger 900#ffd2cadanger 100
warningWarnings, risky but permitted actions#733300warning 600#ffa570warning 200
onWarningContent on a warning fill#fff7f2warning 050#280d00warning 800
warningContainerLow-emphasis warning fill#ffd4bcwarning 100#4c1f00warning 700
onWarningContainerContent on warningContainer#0a0100warning 900#ffd4bcwarning 100
successConfirmation, completion, valid input#00572fsuccess 600#5adb91success 200
onSuccessContent on a success fill#e9fff0success 050#001c0csuccess 800
successContainerLow-emphasis success fill#7dfcb0success 100#00381dsuccess 700
onSuccessContainerContent on successContainer#000501success 900#7dfcb0success 100
infoNeutral information, tips, in-progress states#00478ainfo 600#8fc1ffinfo 200
onInfoContent on an info fill#f4f9ffinfo 050#001632info 800
infoContainerLow-emphasis info fill#c8e1ffinfo 100#002d5cinfo 700
onInfoContainerContent on infoContainer#00030einfo 900#c8e1ffinfo 100

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 the rest of the system. Containers work exactly like primaryContainer: a low-emphasis fill for banners, rows, and tags.

Roles the system does not currently define

These are real gaps, not omissions from this page. If you need one of them, the answer is not to improvise a value.

Need
Current state
What to do today
Secondary actionsNo secondary role is generated.Build secondary buttons from outline (border) plus primary (label) on a transparent or surface fill. Keep it consistent across the product.
LinksNo distinct link role. Links bind to primary.Rely on underline plus primary for link affordance. Do not introduce a separate link color.
Overlay, scrim, elevationNo overlay or elevation token. The system has no shading-based elevation model.Express elevation with outline and surfaceVariant. Modal scrims currently have no system value.

Color hierarchy

In shortWhat makes one thing look like it sits on top of another. In this system that comes from borders and tinted areas, not from shadows or shading.

Because background and surface resolve to the same value, Graphite UI does not build depth by stacking progressively lighter or darker planes. Hierarchy comes from three other mechanisms: containment via outline, emphasis via surfaceVariant, and attention via the accent ramp.

Level
Token
Reads as
Page groundbackgroundThe canvas
Containersurface + outline borderA defined region
Distinct regionsurfaceVariantA field, a hovered or grouped area
Selected / taggedprimaryContainerAccented but not actionable
Primary actionprimaryThe thing to click
Primary contentonBackground / onSurfaceWhat to read first
Secondary contentonSurfaceVariantSupporting detail

Two rules follow. Do not nest more than two surface levels. With one surface value and one variant, a third level has nothing to resolve to. primaryContainer is not a surface. It signals accent state, so a card using it reads as selected, not elevated.

Themes

In shortLight 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. There is one set of names, and each theme fills them differently. onSurfacemeans “main text on a panel” in both, and it comes out near-black in light and near-white in dark.

Role
Light
Dark
Direction
background#f8f8fcneutral 050#121215neutral 18Light ground → dark ground
onBackground#030305neutral 900#dedde2neutral 100Dark text → light text
primary#4c2f93accent 600#beb1ffaccent 200Dark accent → light accent
onPrimary#f8f7ffaccent 050#1a0044accent 800Light label → dark label
outline#63626dneutralVariant 500#807e8bneutralVariant 400Mid → slightly lighter mid

The pattern: light and dark are near-mirror images across the ramp, with content and ground swapping ends and accent inverting with them. Hierarchy survives because the relationships survive: secondary content stays one perceptual step from primary content in both themes, even though the absolute values are opposite.

This is the concrete reason to use semantic tokens rather than values. A hex is correct in exactly one theme. primary is correct in both, on every source color a user picks, without a designer re-checking anything.

Contrast levels

Themes generate at one of two targets, applied to the whole theme rather than per token: AA (4.5:1 text, 3:1 non-text) or AAA (7:1 text, 3:1 non-text). The system is currently generating at AA.

Interaction states

In shortHow colors change when you hover over something, click it, tab to it, or when it is switched off.

A hovered button does not get a different color. It gets the same color, a few steps along its own ramp. That keeps it recognizably the same button and keeps the label readable.

Which direction depends on the theme: steps go darker in light mode and lighter in dark mode, always away from the background, so the button gets more prominent rather than fading into the page.

State
Derivation
Light
Dark
Defaultprimary#4c2f93accent 600#beb1ffaccent 200
Hoverbase ∓ 6 tone#3d1b80accent 34#d0c9ffaccent 86
Pressedbase ∓ 12 tone#2f016daccent 28#e4e0ffaccent 92
Selectedbase ∓ 6 tone#3d1b80accent 34#d0c9ffaccent 86
DisabledNeutral ramp#dedde2neutral 100#2e2d31neutral 700
Disabled contentNeutral ramp#47474bneutral 600#808084neutral 400
FocusAccent ring#664db4accent 500#a08af7accent 300
  • Hover and selected share a tone. They are visually identical by design; selection is distinguished by persistence and supporting affordances (a check, a bold label, a left border), not by color alone.
  • Pressed is twice the hover shift, a clearly larger step, so the press reads as a distinct event rather than a stronger hover.
  • Disabled leaves the accent ramp entirely. Both the fill and its content drop to neutral. This is the one state where an element deliberately loses its brand color. The pairing is intentionally low-contrast and is not contrast-checked. It is exempt under WCAG, and it must never be the only signal that a control is unavailable.
  • Focus is a separate ring token, not a fill change. It does not replace the base color, so a focused button is still recognizably in its current state. Focus stacks with hover, pressed, and selected.

Danger, warning, and success are roles, not states. A field that fails validation takes dangerfor its border and message; it does not get a “danger hover.” Status roles and interaction states compose: a destructive button still hovers and presses along its own ramp.

Accessibility

In shortText has to stand out enough from whatever is behind it to be readable. The system checks this before it hands you a palette, rather than leaving you to test afterwards.

The measure is a contrast ratio, written like 4.5:1. The bigger the number, the easier the text is to read. Fourteen pairings are checked in both themes every time a palette is generated. If one ever fell short, the system nudges that color along its own ramp until it passes, keeping the hue and changing only how light it is.

Pairing
Target
Light
Dark
onPrimary on primary4.5:1
9.20:1 pass
9.70:1 pass
onPrimaryContainer on primaryContainer4.5:1
15.20:1 pass
10.70:1 pass
onSurface on surface4.5:1
19.50:1 pass
13.80:1 pass
onSurfaceVariant on surfaceVariant4.5:1
10.10:1 pass
7.30:1 pass
onBackground on background4.5:1
19.50:1 pass
13.80:1 pass
outline on surface3:1 (UI)
5.70:1 pass
4.70:1 pass
onDanger on danger4.5:1
9.40:1 pass
9.40:1 pass
onDangerContainer on dangerContainer4.5:1
15.00:1 pass
10.50:1 pass
onWarning on warning4.5:1
9.00:1 pass
9.50:1 pass
onWarningContainer on warningContainer4.5:1
15.10:1 pass
10.20:1 pass
onSuccess on success4.5:1
8.30:1 pass
10.20:1 pass
onSuccessContainer on successContainer4.5:1
16.10:1 pass
10.40:1 pass
onInfo on info4.5:1
8.70:1 pass
9.70:1 pass
onInfoContainer on infoContainer4.5:1
15.40:1 pass
10.20:1 pass

What the system does not guarantee

The guarantee covers defined pairings only. Everything outside that list is your responsibility.

  • onSurfaceVariant on surface is not a checked pairing. Secondary text on a default surface is common and usually fine, but verify it.
  • Any cross-pairing you invent is unchecked and likely to fail: onPrimaryContainer on surface, primary as body text, outline as text.
  • Text over images, gradients, or generated patterns. No token can guarantee contrast against variable content; use a solid surface behind the text.
  • Disabled states, which are exempt by design.

Focus indicators

Every interactive element needs a visible focus indicator. Use the focus ring token, which is generated per theme specifically to stay visible against both grounds. Never remove focus outlines, and never let the hover treatment double as the focus state; keyboard users never trigger hover.

Do not rely on color alone

This applies with unusual force here, for two system-specific reasons. The source color is user-chosen. You cannot assume the accent is blue, or warm, or dark, so any meaning attached to a specific hue will be wrong for some sources. And status hue can collide with the source: because status hues are fixed, a source color sitting on one of them resolves primary and that status to nearly the same value. A red brand makes primary and danger near-identical. Always pair color with a second signal: an icon, a label, a change of weight, a position, or a border.

Light and dark parity

Both themes are generated from the same ramps against the same targets, so a design that passes in one passes in the other. But check both before shipping. Anything built outside the token system (illustrations, screenshots, images with baked-in backgrounds, hard-coded hexes) will not follow the theme, and dark mode is where that shows up first.

Usage

Do

  • Assign roles, not values. Reach for primary or onSurfaceVariant, never the hex they currently resolve to.
  • Respect on pairings. onSurface belongs on surface; onPrimary belongs on primary.
  • Use outline and surfaceVariant for depth, since the system has no elevation shading.
  • Check the contrast table when you change the source color, especially at AAA.
  • Pair color with a second signal for any state or status meaning, because 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 a snapshot of one source color in one theme.
  • Don’t reference primitives directly. accent 40 is a color without meaning.
  • Don’t invent status colors from the accent or neutral ramps. Use the status roles.
  • Don’t use primaryContainer as a general surface, because 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 by changing opacity. States are tone shifts on the ramp.

Tokens

In shortHow to find the right named color for what you are building.

Naming

Tokens use camelCase in JSON and JS (onSurfaceVariant), kebab-case in CSS with the --cts- prefix (--cts-on-surface-variant). Every token also emits its provenance: --cts-on-surface-variant-ramp and --cts-on-surface-variant-tone.

Choosing a token

Work down this order and stop at the first match.

  1. What is the element? A ground, a container, content, a border, or an action.
  2. Ground or container? background for the page, surface for a container, surfaceVariant for a field or distinct region.
  3. Content? onX, matching whatever it sits on.
  4. Border? outline, or primary and the focus ring if it indicates interaction.
  5. Action? primary + onPrimary for full emphasis, primaryContainer + onPrimaryContainer for low emphasis.
  6. Communicating status? danger, warning, success, or info, with their containers for low-emphasis fills.
  7. Interactive state? The state token for that role, never a manually adjusted value.
  8. No match? The role is missing from the system. Flag it rather than working around it.

Complete reference

Semantic roles: 27 per theme

primaryonPrimaryprimaryContaineronPrimaryContainersurfaceonSurfacesurfaceVariantonSurfaceVariantoutlinebackgroundonBackgrounddangeronDangerdangerContaineronDangerContainerwarningonWarningwarningContaineronWarningContainersuccessonSuccesssuccessContaineronSuccessContainerinfoonInfoinfoContaineronInfoContainer

Interaction states: 6 per theme

primaryprimary-hoverprimary-pressedprimary-selectedprimary-disabledfocus-ring

Primitives: 7 ramps × 10 stops

Exported for reference and tooling. Available to inspect and copy; not for direct use in designs.

Using tokens in Figma

The engine’s output is CSS and JSON. There is no published Figma library shipping with it today, so the Figma side is a workflow you set up rather than a fact of the system. The recommended approach:

  • Create Figma variables that mirror the semantic role names exactly, so a design file and a code file name the same thing the same way.
  • Use a variable mode per theme (Light / Dark) so a single design switches themes the way the product does.
  • Import primitives as a separate, locked collection: visible for reference, hidden from the picker, so designers select roles rather than stops.
  • Regenerate rather than edit. If the source color changes, re-import from the JSON export instead of hand-adjusting values, or the file will drift from the product.

Pattern reference

In shortTwenty tiles, one rulebook. Every composition draws from the same fixed library, so patterns stay recognizable however they are combined.

Define the rules, let the system execute: the logic behind Sol LeWitt’s wall drawings, applied to interface surfaces.

Color rhythm · 60 / 30 / 10

Sixty percent neutrals, thirty percent accent, ten percent secondary: the ramp the engine derives 120° off your source, so the vivid tenth is generated rather than picked. Adjacent panels check their neighbours so no color clusters.

Span definitions

Larger cells act as anchors. Image panels always land on a large span, distributed across horizontal zones so no region dominates.

  • 1 × 1
  • 2 × 1
  • 1 × 2
  • 2 × 2
  • 3 × 1
  • 3 × 2

Tile library

All twenty types, rendered live from your source color.

  1. 01Diagonal stripes

  2. 02Gesture marks

  3. 03Halftone radial

  4. 04Circle

  5. 05Nested rects

  6. 06Halftone blob

  7. 07Bold diagonal

  8. 08Arc

  9. 09Split diagonal

  10. 10H stripes

  11. 11V stripes

  12. 12Concentric circles

  13. 13Crosshatch

  14. 14Color block

  15. 15Wave lines

  16. 16Dot grid

  17. 17Letterform

  18. 18Checkerboard

  19. 19Eye · halftone

  20. 20Mouth · halftone

Glossary

Source color
The one color you choose. Everything else on this page is calculated from it.
Hue
Which color it is: red, green, blue. Changing hue turns a red into an orange.
Chroma
How intense the color is. High chroma is vivid; zero chroma is gray.
Tone
How light or dark the color is, from 0 (black) to 100 (white). Tone 40 is dark; tone 90 is pale.
Ramp
One hue laid out from dark to light: the same color at ten different tones, like a paint strip.
OKLab
The color model the math runs in. Its useful property: equal steps in numbers look like equal steps to the eye.
Primitive
A raw color on a ramp, with no job attached. Useful to look at, never to build with.
Token / role
A named color with a job, such as “page background” or “button fill”. You build with these.
On-color
The text or icon color that goes on top of another. onSurface is what you put on surface, and it is guaranteed to be readable there.
Container
A quieter version of a color, for filling an area rather than drawing attention. Think tinted banner rather than solid button.
Theme
Light or dark. Same role names in both; different values behind them.
Contrast ratio
How different two colors are in lightness, written like 4.5:1. Higher means easier to read. Below about 4.5:1, small text gets hard for many people.
AA / AAA
Two accessibility bars from the WCAG standard. AA is the common legal minimum; AAA is stricter.