Skip to main content
Graphite UI

Run Graphite locally

For working on Graphite itself. To use Graphite in your own project, start at What you can use today.

Node 24+pnpm 10Webpack required

Requirements

Three things, and one of them is a constraint rather than a version number.

Node24 or newer, the version CI pins.
pnpm10. The lockfile is pnpm’s; npm and yarn will resolve a different tree.
WebpackNot 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-ui

Step 2: Install dependencies

pnpm reads the committed lockfile, so you get the same tree CI does.

pnpm install

Run 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 dev

Why 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.

Live: these set the site’s source color
'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.

Live: switches the site theme
'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>
  )
}
What useTheme returns
FieldTypeWhat it is
theme'light' | 'dark'The active theme. The same two names the exported theme file uses for data-theme.
toggleTheme() => voidFlips between the two. This is what the header’s theme button calls.
setTheme(theme: 'light' | 'dark') => voidSets one directly.
sourceHexstringThe current source, lower-case with a leading #.
setSourceHex(hex: string) => voidChanges the source. Anything that is not a 3- or 6-digit hex is ignored.
lightBundleColorBundle | nullTokens, contrast results and states for the light theme.
darkBundleColorBundle | nullThe same for dark. Both are computed whichever theme is showing.
levelContrastLevelThe contrast target the engine resolves against.
setLevel(level: ContrastLevel) => voidChanges it, and every role re-resolves.
rampsReturnType<typeof makeRamps> | nullThe 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.

CheckCommandGateWhat it proves
drift-checkpnpm drift-checkfatalEvery component references exactly the tokens its contract declares, and nothing else.
token-driftpnpm token-driftfatalThe foundations in globals.scss still match the committed Figma snapshot.
component-doc-driftpnpm component-doc-driftfatalEvery 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.