Skip to main content
Graphite UI

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.

8 ramps32 rolesLight + Dark

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.

  1. Your colorOne hex. The engine reads three things from it: which color it is, how intense, and how light.
  2. RampsEight strips of ten shades each. Four follow your color, four carry status.
  3. RolesThirty-two named jobs per theme, such as page background, body text and button fill. Each takes a shade and is checked for readability.
  4. StatesHover, pressed, selected, disabled and focus for the primary, secondary and danger families, as steps on their own ramps.
  5. 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.

The eight ramps
RampIntensityWhat it is for
accentSame as your colorBrand color, interactive elements, focus
secondaryJust over half, 120° round the wheelThe counterpoint: the vivid tenth of 60/30/10
neutralVariantBarely tintedBorders and supporting text
neutralAlmost grayPage backgrounds, surfaces, primary text
danger / warning / success / infoTracks your color, within limitsStatus 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

Surfaces and backgrounds
RolePurposeLightDark
surfaceDefault container: cards, panels, sheets#f8f8fcneutral 050#121215neutral 18
surfaceElevatedOne step up from surface, for what floats: menus, popovers, the modal panel#ffffffneutral 100#201f23neutral 24
surfaceVariantSecondary surface: fields, hover fills, selected rows#dedceaneutralVariant 100#2e2c37neutralVariant 700
backgroundThe page itself#f8f8fcneutral 050#121215neutral 18

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

Content: text and icons
RolePurposeLightDark
onSurfaceContent on a default surface#030305neutral 900#dedde2neutral 100
onSurfaceVariantSecondary text and icons, supporting copy#2e2c37neutralVariant 700#bdbcc9neutralVariant 200
onBackgroundPrimary text and icons on the page#030305neutral 900#dedde2neutral 100

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

Borders

Borders
RolePurposeLightDark
outlineBorders and dividers#63626dneutralVariant 500#807e8bneutralVariant 400

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

Primary and secondary actions
RolePurposeLightDark
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
secondaryThe counterpoint accent: secondary buttons, the vivid tenth of 60/30/10#005542secondary 600#7dd1b6secondary 200
onSecondaryContent on a secondary fill#e7fff6secondary 050#001c14secondary 800
secondaryContainerLow-emphasis secondary fill#9ef2d6secondary 100#00372asecondary 700
onSecondaryContainerContent on secondaryContainer#000503secondary 900#9ef2d6secondary 100

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

Status and feedback
RolePurposeLightDark
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: banners, rows#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: banners, rows#7dfcb0success 100#00381dsuccess 700
onSuccessContainerContent on successContainer#000501success 900#7dfcb0success 100
infoNeutral information, tips, in-progress states#00478ainfo 600#8fc1ffinfo 200
onInfoContent on a info fill#f4f9ffinfo 050#001632info 800
infoContainerLow-emphasis info fill: banners, rows#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 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 primary and keep their underline, so color is never the only affordance.
  • The scrim. It is not a role but it is generated: --graphite-scrim is 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).

Hierarchy levels
LevelRoleReads as
Page groundbackgroundThe canvas
Containersurface + outlineA defined region
FloatingsurfaceElevatedSomething over the page: a menu, a dialog
Distinct regionsurfaceVariantA field, a hovered or grouped area
Selected or taggedprimaryContainerAccented but not actionable
Primary actionprimaryThe thing to click
Primary contentonBackground / onSurfaceWhat to read first
Secondary contentonSurfaceVariantSupporting 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.

One role, two themes
RoleLightDarkDirection
background#f8f8fcneutral 050#121215neutral 18Light ground to dark ground
onBackground#030305neutral 900#dedde2neutral 100Dark text to light text
primary#4c2f93accent 600#beb1ffaccent 200Dark accent to light accent
onPrimary#f8f7ffaccent 050#1a0044accent 800Light label to dark label
outline#63626dneutralVariant 500#807e8bneutralVariant 400Mid 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.

Interaction states of primary
StateDerivationLightDark
DefaultThe role itself#4c2f93accent 600#beb1ffaccent 200
Hover6 tone steps#3d1b80accent 34#d0c9ffaccent 86
Pressed12 tone steps#2f016daccent 28#e4e0ffaccent 92
Selected6 tone steps#3d1b80accent 34#d0c9ffaccent 86
DisabledNeutral ramp#dedde2neutral 100#2e2d31neutral 700
Disabled contentNeutral ramp#47474bneutral 600#808084neutral 400
FocusA ring on the same ramp#664db4accent 500#a08af7accent 300
  • 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.

Contrast pairings in both themes
PairingTargetLightDark
onDanger on danger4.5:19.4:1Pass9.4:1Pass
onDangerContainer on dangerContainer4.5:115.0:1Pass10.5:1Pass
onWarning on warning4.5:19.0:1Pass9.5:1Pass
onWarningContainer on warningContainer4.5:115.1:1Pass10.2:1Pass
onSuccess on success4.5:18.3:1Pass10.2:1Pass
onSuccessContainer on successContainer4.5:116.1:1Pass10.4:1Pass
onInfo on info4.5:18.7:1Pass9.7:1Pass
onInfoContainer on infoContainer4.5:115.4:1Pass10.2:1Pass
onPrimary on primary4.5:19.2:1Pass9.7:1Pass
onPrimaryContainer on primaryContainer4.5:115.2:1Pass10.7:1Pass
onSecondary on secondary4.5:18.4:1Pass9.9:1Pass
onSecondaryContainer on secondaryContainer4.5:115.8:1Pass10.2:1Pass
onPrimary on secondary4.5:18.3:1Pass10.3:1Pass
onPrimary on danger4.5:19.4:1Pass9.5:1Pass
onSurface on surface4.5:119.5:1Pass13.8:1Pass
onSurfaceVariant on surfaceVariant4.5:110.1:1Pass7.3:1Pass
onBackground on background4.5:119.5:1Pass13.8:1Pass
outline on surface3:1 (UI)5.7:1Pass4.7:1Pass

What is not guaranteed

  • onSurfaceVariant on surface is not a checked pairing. It is common and usually fine, but verify it.
  • Any cross-pairing you invent: onPrimaryContainer on surface, primary as body text, outline as 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.

  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, surfaceElevated for something floating, surfaceVariant for a field or distinct region.
  3. Content? The on- role matching whatever it sits on.
  4. Border? outline, or primary and the focus ring if it marks interaction.
  5. Action? primary with onPrimary for full emphasis, secondary for the counterpoint, a container pair for low emphasis.
  6. Status? danger, warning, success or info, with their containers for quiet fills.
  7. Interactive state? The state variable for that family, never a hand-adjusted value.
  8. 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.