Skip to main content
Graphite UI

Governance

Graphite exists three times: as a Figma kit, as a written contract per component, and as React code. Governance is what keeps the three saying the same thing. It is a solo-maintainer model, so it relies on a fixed place where truth lives and scripts that check reality against it, not on review gates.

36 governed components

Contracts

A contract is a Markdown file whose frontmatter says what a component is: what it contains, what it accepts, which tokens it may use, and what it must never do.

Contract frontmatter fields
slotsThe parts a caller fills, and whether each is required.
propsThe component’s API: each prop, its values, and why.
tokensThe only roles the code may reference. The drift check holds the implementation to this list exactly.
composition_rulesHow it behaves alongside other components.
prohibitionsWhat it must never do. Changing one is a breaking change.

Each also carries component, version and wave. Button is the reference implementation, at 2.5.0: one slot, five props, nine tokens and three prohibitions. Its prohibitions, as the contract states them:

  • No two primary buttons in one group. The second one is not an emphasis choice, it is a missing decision about which action the group is for.
  • No destructive action on the primary variant. Destructive work takes danger, so that "the emphasised action" and "the dangerous action" never look like the same thing.
  • No button whose accessible name comes only from an icon. The icon size changes the shape, never the naming requirement.

Read the whole file at docs/contracts/button.md, or see it rendered as the Button page, whose API and token tables are read from it.

Every contract, read from docs/contracts
ContractVersionWaveDeclaresComposition
Buttonv2.5.0Wave 01 slot · 5 props · 9 tokens4 rules · 3 prohibitions
Button Groupv1.0.1Wave 01 slot · 1 prop · 1 token3 rules · 3 prohibitions
Menu buttonsv1.0.1Wave 02 slots · 7 props · 1 token3 rules · 2 prohibitions
Progress barv2.0.0Wave 12 slots · 9 props · 10 tokens1 rule · 2 prohibitions
Tagv3.2.0Wave 12 slots · 9 props · 28 tokens1 rule · 2 prohibitions
Typographyv1.2.0Wave 11 slot · 3 props · 4 tokens2 rules · 1 prohibition
Checkboxv3.1.1Wave 22 slots · 10 props · 9 tokens3 rules · 1 prohibition
Checkbox groupv1.0.0Wave 23 slots · 7 props · 8 tokens2 rules · 2 prohibitions
Date pickerv1.0.0Wave 24 slots · 9 props · 11 tokens3 rules · 2 prohibitions
Dropdownv1.2.2Wave 24 slots · 9 props · 15 tokens5 rules · 2 prohibitions
File uploaderv1.0.0Wave 24 slots · 7 props · 11 tokens3 rules · 2 prohibitions
Linkv1.0.0Wave 22 slots · 6 props · 6 tokens3 rules · 2 prohibitions
Number inputv1.0.0Wave 22 slots · 3 props · 2 tokens3 rules · 2 prohibitions
Password inputv1.0.0Wave 22 slots · 2 props · 2 tokens3 rules · 2 prohibitions
Radio button groupv2.1.0Wave 23 slots · 8 props · 9 tokens2 rules · 1 prohibition
Searchv1.0.0Wave 23 slots · 6 props · 10 tokens3 rules · 1 prohibition
Selectv3.0.0Wave 23 slots · 9 props · 12 tokens3 rules · 1 prohibition
Sliderv1.0.0Wave 25 slots · 7 props · 11 tokens3 rules · 2 prohibitions
Text areav3.0.0Wave 25 slots · 8 props · 12 tokens2 rules · 2 prohibitions
Text inputv2.3.2Wave 25 slots · 10 props · 12 tokens2 rules · 1 prohibition
Time pickerv1.0.0Wave 25 slots · 8 props · 7 tokens2 rules · 2 prohibitions
Togglev3.0.0Wave 23 slots · 10 props · 11 tokens2 rules · 1 prohibition
Accordionv1.1.0Wave 44 slots · 6 props · 7 tokens3 rules · 5 prohibitions
Breadcrumbv1.3.0Wave 42 slots · 3 props · 4 tokens2 rules · 1 prohibition
Contained listv2.1.0Wave 46 slots · 4 props · 9 tokens3 rules · 1 prohibition
Navigation Menuv2.0.1Wave 42 slots · 1 prop · 4 tokens2 rules · 2 prohibitions
Tabsv2.0.0Wave 43 slots · 7 props · 10 tokens3 rules · 1 prohibition
Tree viewv1.0.0Wave 43 slots · 7 props · 7 tokens3 rules · 2 prohibitions
Menuv2.1.0Wave 54 slots · 5 props · 12 tokens4 rules · 1 prohibition
Modalv2.0.0Wave 56 slots · 3 props · 8 tokens5 rules · 1 prohibition
Notificationv3.0.1Wave 53 slots · 5 props · 17 tokens3 rules · 1 prohibition
Overlayv2.0.0Wave 50 slots · 2 props · 0 tokens8 rules · 1 prohibition
Popoverv2.0.0Wave 52 slots · 6 props · 5 tokens4 rules · 1 prohibition
Toastv1.0.0Wave 56 slots · 6 props · 18 tokens3 rules · 2 prohibitions
Tooltipv2.0.0Wave 52 slots · 4 props · 9 tokens5 rules · 1 prohibition
Data tablev2.0.1Wave 65 slots · 7 props · 11 tokens2 rules · 1 prohibition
Paginationv1.0.0Wave 64 slots · 5 props · 7 tokens3 rules · 1 prohibition

Thirty-seven contracts: the thirty-six governed components and the Overlay hook they share, read from their frontmatter when this page was built. Inherited rows are counted in full, so Text area’s figures include what it takes from Text input. The foundations (spacing, radius, breakpoint, type) have contracts too, in docs/contracts/foundations/, and answer to token-drift instead.

The rules

Numbered as the contracts README numbers them. That file is the authority; this is the same list in plainer words.

  1. Rule 1: Every component has one contract file, docs/contracts/<component>.md, in the same repo as the site. Read more

  2. Rule 2: No component code changes without a matching contract update first, even for one-line fixes.

  3. Rule 3: Each contract is versioned with semver. A prohibition change is breaking, a new optional slot is minor, and a copy or description edit is a patch.

  4. Rule 4: A drift check reads each contract’s declared tokens and verifies the component’s code references exactly those variables, and nothing else. It fails the build on a mismatch. Read more

  5. Rule 5: Figma components carry their contract version in the description field, so anyone opening the file knows which spec they are looking at.

  6. Rule 6: Every component set in the kit is either governed (a contract declares it, and its description carries that contract’s version) or ungoverned, and says so in the same place. Nothing is unlabelled. Read more

  7. Rule 7: Where the kit and a contract disagree, the kit wins: correct the contract, not the kit. Where the kit disagrees with itself, the more specific artefact wins. Where the kit has no opinion, the code keeps its own. Read more

  8. Rule 8: Rules 6 and 7 say who wins; neither says what should exist. A governed component with no counterpart in the kit is kept only while something needs it, and rule 6’s demand test decides. Read more

The README lists eight rules today, and this page checks that when it is built. Read the README.

The kit is canonical

Rule 7. Where the Figma kit and a contract disagree, the kit wins and the contract is corrected.

This reversed on 2026-08-28. Until then the contract was canonical and Figma and the site both implemented it. Contracts are still the written spec the code is checked against, and a component still may not change without its contract changing first. What moved is precedence: a contract now describes the kit rather than outranking it, so a disagreement is a bug in the contract. In practice the site’s components look like the kit’s. For Button that meant square corners, Carbon’s asymmetric 0 64px 0 16px inset, a filled secondary, and primary as ghost’s label.

When the kit is not of one mind

Rule 7 settles kit against contract. It does not settle kit against kit, which came up three times in one pass, so the tie-break is written down rather than re-derived.

The tie-break and its precedents
CaseRuleWhat won
Input label sizeMore specific winsThe type specimen says 12/12; every form component renders 12/16. The components won: they are what a label actually looks like.
Overlay surfaceMore specific winssurfaceElevated is described as the overlay surface, while the overlay sets fill with Layer/layer-01. The variable won: it was authored for those four components, and the bindings are un-migrated Carbon.
Data table headerKit has no opinionTransparent in the kit, but the code’s header is sticky, and a transparent sticky header lets rows show through. The kit does not model scrolling, so the code keeps surface.
Hover, focus, pressedKit has no opinionVariant axes in the kit, pseudo-classes in code. A State=Hover variant is not an instruction to add a hover prop.

Governed and ungoverned

Rule 6. A set is governed if a contract declares it, and ungoverned otherwise. Ungoverned sets stay in the kit and say so.

Kit pages
45
Component sets
206
Public sets
115
Contracts
37

Counted from the committed kit snapshot and the contracts directory. The _ prefix is the kit’s own line between public and private: private sets carry “Do not edit” and are internals the public ones are built from, so the labelling duty applies to public sets only. The disposition walk of 2026-08-28 found 27 of the kit’s pages claimed by no contract, holding 132 sets (73 public, 59 private).

Labelling beats deleting. The harm an ungoverned set does is that a designer cannot tell it from a governed one, and saying so in its description is the whole fix. The library is published, so removing a set is a breaking change for anyone consuming it. Movement is one way: a set becomes governed by acquiring a contract, and nothing goes back. Contained list was the first to move; Accordion was the first adoption the rule decided.

Where an ungoverned set falls
BucketExamplesWhat happens
Application shellsUI shell, Content switcherOut of scope permanently. They compose an application; they are not primitives.
Vendor featuresAI label, AI layer, AI explainability popoverOut of scope permanently. Tied to IBM product decisions Graphite does not make.
Carbon idiomsStructured list, Toggletip, Tile, Code snippet, Loading, Progress indicator, Form, ListOut of scope permanently. No Graphite counterpart.
Built and governedLink, Search, Slider, Pagination, Date picker, Time picker, File uploader, Number and Password input, Dropdown, Menu buttonsOnce awaiting demand or folded into another contract. Built with their own contracts on 2026-10-04, so they are governed now.

What should exist

Rule 8. Rules 6 and 7 say who wins a disagreement. Neither says whether a component should exist at all.

  1. Does the kit have a governed counterpart? Rule 7 applies: the kit wins, and the code is brought to it.
  2. If not, does the kit answer the same need inside something it governs? Then absorb it there. The kit ships no Label set, but it has a firm opinion about where a label lives: on the control.
  3. Does the kit say nothing at all? The contract is authoritative, and rule 6’s demand test decides whether the component is kept.
The six components rule 8 was written for
ComponentQuestionDemandOutcome
Separator, Avatar, Card3None; only the gallery composed themRemoved
Label, Field2Answered on the form controls insteadAbsorbed
Navigation Menu3site-header.tsx, and step 1 of the shadcn migrationKept
Typography3Two contracts depend on it, one in a prohibitionKept

Drift checks

Three scripts, each comparing one pair of artefacts. None of them talks to Figma: the Figma-backed two read a committed snapshot, so all three run offline.

The governance checks
CheckComparesWhat it proves
drift-checkComponents against their contractsEvery component references exactly the --graphite-* variables its contract’s tokens produce, and nothing else. It asks lib/color.js what the engine emits, so a contract cannot declare a role that does not exist. --cds-* references warn rather than fail.
token-driftglobals.scss against docs/tokens/figma-snapshot.jsonThe foundations the browser gets (spacing, radius, breakpoints, type) still say what the kit says. A difference is drift whichever side moved; the check cannot tell which.
component-doc-driftdocs/components/*.md against docs/tokens/figma-components.jsonEvery Figma node id a doc cites still resolves, and every public set on a page a doc covers is named or cited in that doc. This is what catches the kit gaining a set nobody wrote up.
pnpm drift-check
pnpm token-drift
pnpm component-doc-drift

CI and main

The checks run as one job, governance, and main will not accept a change without it.

Steps of the governance job, read from checks.yml
StepRuns
Typecheckpnpm typecheck
Drift check: components against their contractspnpm drift-check
Token drift: foundations against the Figma snapshotpnpm token-drift
Token drift self-testpnpm token-drift:test
Component doc drift: docs/components against the kit snapshotpnpm component-doc-drift
Component doc drift self-testpnpm component-doc-drift:test
Color engine test: Button's label pairspnpm color:test
Naming check: --graphite-* is the only prefixpnpm naming-check
Registry check: every item builds and its dependencies resolvepnpm registry-check

Read from .github/workflows/checks.yml. Every step runs even if an earlier one failed, so a contract change that trips more than one check reports them together. The job deliberately does not run next build: Vercel already does, and a green build proves the site compiles, not that it matches its spec.

Branch protection on main
Pull requestRequired for every change, one-line doc edits included. Zero approvals, because a solo maintainer cannot approve their own pull request.
Status checkgovernance must pass. Vercel is not the gate.
AdminsBound too. The one unreviewed change in recent history was the owner pushing straight to main, and an admin bypass would have allowed it.
HistoryLinear, with force pushes and branch deletion off. Up to date before merging is not required: with one pull request in flight it only buys needless rebases.

If CI is ever broken badly enough to block its own fix, the way out is to disable protection, land the fix and re-enable it: a deliberate act that leaves a trace, which is what having no bypass is meant to cost.

Next steps

The contracts, the components they govern, and the checks in practice.

  • Contracts README

    The authority for everything on this page, including the build order and the component API conventions.

  • Components

    Thirty-six governed components, each page generated from its contract.

  • Run Graphite locally

    Running the checks locally before you open a pull request.