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.
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.
| slots | The parts a caller fills, and whether each is required. |
|---|---|
| props | The component’s API: each prop, its values, and why. |
| tokens | The only roles the code may reference. The drift check holds the implementation to this list exactly. |
| composition_rules | How it behaves alongside other components. |
| prohibitions | What 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
iconsize 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.
| Contract | Version | Wave | Declares | Composition |
|---|---|---|---|---|
| Button | v2.5.0 | Wave 0 | 1 slot · 5 props · 9 tokens | 4 rules · 3 prohibitions |
| Button Group | v1.0.1 | Wave 0 | 1 slot · 1 prop · 1 token | 3 rules · 3 prohibitions |
| Menu buttons | v1.0.1 | Wave 0 | 2 slots · 7 props · 1 token | 3 rules · 2 prohibitions |
| Progress bar | v2.0.0 | Wave 1 | 2 slots · 9 props · 10 tokens | 1 rule · 2 prohibitions |
| Tag | v3.2.0 | Wave 1 | 2 slots · 9 props · 28 tokens | 1 rule · 2 prohibitions |
| Typography | v1.2.0 | Wave 1 | 1 slot · 3 props · 4 tokens | 2 rules · 1 prohibition |
| Checkbox | v3.1.1 | Wave 2 | 2 slots · 10 props · 9 tokens | 3 rules · 1 prohibition |
| Checkbox group | v1.0.0 | Wave 2 | 3 slots · 7 props · 8 tokens | 2 rules · 2 prohibitions |
| Date picker | v1.0.0 | Wave 2 | 4 slots · 9 props · 11 tokens | 3 rules · 2 prohibitions |
| Dropdown | v1.2.2 | Wave 2 | 4 slots · 9 props · 15 tokens | 5 rules · 2 prohibitions |
| File uploader | v1.0.0 | Wave 2 | 4 slots · 7 props · 11 tokens | 3 rules · 2 prohibitions |
| Link | v1.0.0 | Wave 2 | 2 slots · 6 props · 6 tokens | 3 rules · 2 prohibitions |
| Number input | v1.0.0 | Wave 2 | 2 slots · 3 props · 2 tokens | 3 rules · 2 prohibitions |
| Password input | v1.0.0 | Wave 2 | 2 slots · 2 props · 2 tokens | 3 rules · 2 prohibitions |
| Radio button group | v2.1.0 | Wave 2 | 3 slots · 8 props · 9 tokens | 2 rules · 1 prohibition |
| Search | v1.0.0 | Wave 2 | 3 slots · 6 props · 10 tokens | 3 rules · 1 prohibition |
| Select | v3.0.0 | Wave 2 | 3 slots · 9 props · 12 tokens | 3 rules · 1 prohibition |
| Slider | v1.0.0 | Wave 2 | 5 slots · 7 props · 11 tokens | 3 rules · 2 prohibitions |
| Text area | v3.0.0 | Wave 2 | 5 slots · 8 props · 12 tokens | 2 rules · 2 prohibitions |
| Text input | v2.3.2 | Wave 2 | 5 slots · 10 props · 12 tokens | 2 rules · 1 prohibition |
| Time picker | v1.0.0 | Wave 2 | 5 slots · 8 props · 7 tokens | 2 rules · 2 prohibitions |
| Toggle | v3.0.0 | Wave 2 | 3 slots · 10 props · 11 tokens | 2 rules · 1 prohibition |
| Accordion | v1.1.0 | Wave 4 | 4 slots · 6 props · 7 tokens | 3 rules · 5 prohibitions |
| Breadcrumb | v1.3.0 | Wave 4 | 2 slots · 3 props · 4 tokens | 2 rules · 1 prohibition |
| Contained list | v2.1.0 | Wave 4 | 6 slots · 4 props · 9 tokens | 3 rules · 1 prohibition |
| Navigation Menu | v2.0.1 | Wave 4 | 2 slots · 1 prop · 4 tokens | 2 rules · 2 prohibitions |
| Tabs | v2.0.0 | Wave 4 | 3 slots · 7 props · 10 tokens | 3 rules · 1 prohibition |
| Tree view | v1.0.0 | Wave 4 | 3 slots · 7 props · 7 tokens | 3 rules · 2 prohibitions |
| Menu | v2.1.0 | Wave 5 | 4 slots · 5 props · 12 tokens | 4 rules · 1 prohibition |
| Modal | v2.0.0 | Wave 5 | 6 slots · 3 props · 8 tokens | 5 rules · 1 prohibition |
| Notification | v3.0.1 | Wave 5 | 3 slots · 5 props · 17 tokens | 3 rules · 1 prohibition |
| Overlay | v2.0.0 | Wave 5 | 0 slots · 2 props · 0 tokens | 8 rules · 1 prohibition |
| Popover | v2.0.0 | Wave 5 | 2 slots · 6 props · 5 tokens | 4 rules · 1 prohibition |
| Toast | v1.0.0 | Wave 5 | 6 slots · 6 props · 18 tokens | 3 rules · 2 prohibitions |
| Tooltip | v2.0.0 | Wave 5 | 2 slots · 4 props · 9 tokens | 5 rules · 1 prohibition |
| Data table | v2.0.1 | Wave 6 | 5 slots · 7 props · 11 tokens | 2 rules · 1 prohibition |
| Pagination | v1.0.0 | Wave 6 | 4 slots · 5 props · 7 tokens | 3 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.
Rule 1: Every component has one contract file,
docs/contracts/<component>.md, in the same repo as the site. Read moreRule 2: No component code changes without a matching contract update first, even for one-line fixes.
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.
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
Rule 5: Figma components carry their contract version in the description field, so anyone opening the file knows which spec they are looking at.
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
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
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.
| Case | Rule | What won |
|---|---|---|
| Input label size | More specific wins | The type specimen says 12/12; every form component renders 12/16. The components won: they are what a label actually looks like. |
| Overlay surface | More specific wins | surfaceElevated 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 header | Kit has no opinion | Transparent 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, pressed | Kit has no opinion | Variant 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.
| Bucket | Examples | What happens |
|---|---|---|
| Application shells | UI shell, Content switcher | Out of scope permanently. They compose an application; they are not primitives. |
| Vendor features | AI label, AI layer, AI explainability popover | Out of scope permanently. Tied to IBM product decisions Graphite does not make. |
| Carbon idioms | Structured list, Toggletip, Tile, Code snippet, Loading, Progress indicator, Form, List | Out of scope permanently. No Graphite counterpart. |
| Built and governed | Link, Search, Slider, Pagination, Date picker, Time picker, File uploader, Number and Password input, Dropdown, Menu buttons | Once 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.
- Does the kit have a governed counterpart? Rule 7 applies: the kit wins, and the code is brought to it.
- 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.
- Does the kit say nothing at all? The contract is authoritative, and rule 6’s demand test decides whether the component is kept.
| Component | Question | Demand | Outcome |
|---|---|---|---|
| Separator, Avatar, Card | 3 | None; only the gallery composed them | Removed |
| Label, Field | 2 | Answered on the form controls instead | Absorbed |
| Navigation Menu | 3 | site-header.tsx, and step 1 of the shadcn migration | Kept |
| Typography | 3 | Two contracts depend on it, one in a prohibition | Kept |
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.
| Check | Compares | What it proves |
|---|---|---|
| drift-check | Components against their contracts | Every 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-drift | globals.scss against docs/tokens/figma-snapshot.json | The 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-drift | docs/components/*.md against docs/tokens/figma-components.json | Every 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-driftCI and main
The checks run as one job, governance, and main will not accept a change without it.
| Step | Runs |
|---|---|
| Typecheck | pnpm typecheck |
| Drift check: components against their contracts | pnpm drift-check |
| Token drift: foundations against the Figma snapshot | pnpm token-drift |
| Token drift self-test | pnpm token-drift:test |
| Component doc drift: docs/components against the kit snapshot | pnpm component-doc-drift |
| Component doc drift self-test | pnpm component-doc-drift:test |
| Color engine test: Button's label pairs | pnpm color:test |
| Naming check: --graphite-* is the only prefix | pnpm naming-check |
| Registry check: every item builds and its dependencies resolve | pnpm 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.
| Pull request | Required for every change, one-line doc edits included. Zero approvals, because a solo maintainer cannot approve their own pull request. |
|---|---|
| Status check | governance must pass. Vercel is not the gate. |
| Admins | Bound too. The one unreviewed change in recent history was the owner pushing straight to main, and an admin bypass would have allowed it. |
| History | Linear, 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.
