Run Graphite locally
For working on Graphite itself. To use Graphite in your own project, start at What you can use today.
Requirements
Three things, and one of them is a constraint rather than a version number.
| Node | 24 or newer, the version CI pins. |
|---|---|
| pnpm | 10. The lockfile is pnpm’s; npm and yarn will resolve a different tree. |
| Webpack | Not optional. The dev server must run with --webpack, because Turbopack breaks on this project’s Sass. |
Create the project
Two commands. The lockfile is pnpm’s, so use pnpm: npm or yarn will resolve a different tree and the Sass will not match.
Step 1: Clone the repository
Everything below assumes you are in the project root.
git clone https://github.com/ssimorka/graphite-ui.git
cd graphite-uiStep 2: Install dependencies
pnpm reads the committed lockfile, so you get the same tree CI does.
pnpm installRun it
One command, and one flag you cannot drop.
Step 3: Start the dev server
It starts on port 3000, or on the next free port if another checkout already holds 3000. Read the address it prints before you trust what the browser shows you: the old tab may still be the other checkout.
pnpm devWhy webpack
Because the alternative silently produces a different site.
Working in the repo
The site holds one source color and one theme, and every page reads them.
Step 4: Set the source
The swatch in the header sets it, and so does Create. In code, the source lives in the theme provider and setSourceHex changes it. The browser saves it, so every page reads the same color. The default is #5E44AA, sampled from the kit’s cover image.
'use client'
import { useTheme, COVER_SOURCE_HEX } from '@/components/theme-provider'
import { Button } from '@/components/ui/button'
import styles from './source-picker.module.scss'
const SOURCES = [COVER_SOURCE_HEX, '#0f62fe', '#007d79']
export function SourcePicker() {
const { sourceHex, setSourceHex } = useTheme()
return (
<div className={styles.sources} role="group" aria-label="Source color">
{SOURCES.map((hex) => (
<Button
key={hex}
variant={hex === sourceHex ? 'primary' : 'secondary'}
aria-pressed={hex === sourceHex}
onClick={() => setSourceHex(hex)}
>
{hex.toUpperCase()}
</Button>
))}
</div>
)
}.sources {
display: flex;
flex-wrap: wrap;
gap: var(--graphite-space-03);
}Step 5: Read the theme from useTheme
useTheme is the hook the header’s theme button uses. It needs a client component.
'use client'
import { useTheme } from '@/components/theme-provider'
import { Button } from '@/components/ui/button'
export function ThemeSwitch() {
const { theme, toggleTheme } = useTheme()
return (
<Button onClick={toggleTheme} aria-pressed={theme === 'dark'}>
Dark theme
</Button>
)
}| Field | Type | What it is |
|---|---|---|
| theme | 'light' | 'dark' | The active theme. The same two names the exported theme file uses for data-theme. |
| toggleTheme | () => void | Flips between the two. This is what the header’s theme button calls. |
| setTheme | (theme: 'light' | 'dark') => void | Sets one directly. |
| sourceHex | string | The current source, lower-case with a leading #. |
| setSourceHex | (hex: string) => void | Changes the source. Anything that is not a 3- or 6-digit hex is ignored. |
| lightBundle | ColorBundle | null | Tokens, contrast results and states for the light theme. |
| darkBundle | ColorBundle | null | The same for dark. Both are computed whichever theme is showing. |
| level | ContrastLevel | The contrast target the engine resolves against. |
| setLevel | (level: ContrastLevel) => void | Changes it, and every role re-resolves. |
| ramps | ReturnType<typeof makeRamps> | null | The ramps for the current source, keyed by name. |
Checks
Three governance checks run in CI, and all three read a committed snapshot rather than the network, so they work offline. This is the part of Graphite that has no equivalent elsewhere.
| Check | Command | Gate | What it proves |
|---|---|---|---|
| drift-check | pnpm drift-check | fatal | Every component references exactly the tokens its contract declares, and nothing else. |
| token-drift | pnpm token-drift | fatal | The foundations in globals.scss still match the committed Figma snapshot. |
| component-doc-drift | pnpm component-doc-drift | fatal | Every public component set in the kit is named or cited by a doc, and every doc’s node ids still resolve. |
pnpm typecheck, the two checker self-tests, pnpm naming-check, which keeps --graphite-* the only variable prefix, and pnpm registry-check, which builds every registry item, run in the same required governance job. main is protected, so every change lands through a pull request with that job green, including one-line doc edits.
Next steps
The rest of Contribute.
- Governance
The eight rules, and why the kit outranks the contracts.
- Snapshots and drift
How the checks compare code to the kit, and what they cannot see.
- Carbon migration
What still comes from Carbon, and the plan to remove it.
