Quick start
Graphite in your own project, from a color to a themed screen.
Before you start
- Node 20 or newer.
- An existing Next.js project on the App Router, in TypeScript.
npx create-next-app@latestgives you one. - The
@/import alias, whichcreate-next-appsets up by default. - Tailwind v4, only if you want the Tailwind classes in step 4.
Another React setup, such as Vite, works too. Put the imports from steps 2 to 4 in your entry file (for example src/main.tsx) instead of app/layout.tsx, and set up the @/ alias in tsconfig.json and your bundler.
Step 1: Add the theme
Run this in your project root. Put your color in place of 0f766e: six hex digits, no #.
npx shadcn@latest add https://www.graphite-ui.com/r/init.json https://www.graphite-ui.com/r/theme/0f766e.json https://www.graphite-ui.com/r/tailwind.jsonIt writes three files: components.json in the project root, and app/graphite-theme.css and app/graphite-tailwind.css. The theme meets the AA contrast target. For AAA, add ?level=AAA to the theme URL and put that URL in quotes.
The theme is one plain CSS file: foundations (space, radius, motion, type), then colors for light and dark, all as --graphite-* variables.
:root, [data-theme="light"] {
color-scheme: light;
--graphite-primary: #4c2f93;
--graphite-on-primary: #f8f7ff;
/* … */To pick the color by eye, or to change radius, density or type too, use Create instead. Get the code downloads graphite-theme.css (and graphite-tailwind.css from its Tailwind tab), at the contrast target you chose there. Save them in app/, then run the init item alone so components can install:
npx shadcn@latest add https://www.graphite-ui.com/r/init.jsonStep 2: Load the fonts
The theme names IBM Plex but does not load it. Install it from Fontsource, which keeps the real font names. next/font renames fonts, so the theme would not find them.
npm i @fontsource/ibm-plex-sans @fontsource/ibm-plex-monoImport the weights in app/layout.tsx, as step 3 shows.
Step 3: Import the theme
Import graphite-theme.css once. Without Tailwind, import it in app/layout.tsx, after the fonts. With Tailwind, import it in globals.css instead (step 4), not in both.
Set data-theme="light" on <html> as the default, so the server and the browser agree. Remove it to follow the visitor’s OS.
In a new create-next-app project, delete the starter’s :root, @theme and body rules from globals.css: they override the theme.
// app/layout.tsx
import '@fontsource/ibm-plex-sans/400.css'
import '@fontsource/ibm-plex-sans/500.css'
import '@fontsource/ibm-plex-sans/600.css'
import '@fontsource/ibm-plex-sans/700.css'
import '@fontsource/ibm-plex-mono/400.css'
import './graphite-theme.css' // with Tailwind, in globals.css instead
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en" data-theme="light">
<body>{children}</body>
</html>
)
}Step 4: Use Tailwind (optional)
On Tailwind v4, import the theme and then graphite-tailwind.css in globals.css. The Tailwind file holds no values, so dark theme and a new color reach every class. Write bg-surface, text-on-primary, p-space-05 or text-body-3. Spacing keeps a space- prefix because the kit’s 05 is 16px, where Tailwind’s p-5 is 20px. The kit’s breakpoints are in the file too, commented out; uncomment them to replace Tailwind’s.
/* app/globals.css */
@import "tailwindcss";
@import "./graphite-theme.css";
@import "./graphite-tailwind.css";
/* then, in markup */
<div className="bg-surface text-on-surface p-space-05 rounded-none">
<h2 className="font-1 text-heading-5">Graphite on Tailwind</h2>
</div>Step 5: Style with the roles
CSS modules in .module.scss need sass: run npm i -D sass first. Step 7 adds it too, but your own module comes before that.
Use a role, never a hex. Here the container role and its on- partner are a checked pairing, so the text clears the contrast target for any color, in either theme.
import styles from './release-note.module.scss'
export function ReleaseNote() {
return (
<aside className={styles.note}>
<p className={styles.title}>Theme updated</p>
<p className={styles.body}>Every role below follows the source color.</p>
</aside>
)
}.note {
padding: var(--graphite-space-05);
background: var(--graphite-primary-container);
border-left: 3px solid var(--graphite-primary);
}
.title {
margin: 0 0 var(--graphite-space-02);
font-size: var(--graphite-text-title-4-size);
font-weight: var(--graphite-text-weight-semibold);
line-height: var(--graphite-text-title-4-line-height);
color: var(--graphite-on-primary-container);
}
.body {
margin: 0;
font-size: var(--graphite-text-body-2-size);
line-height: var(--graphite-text-body-2-line-height);
color: var(--graphite-on-primary-container);
}| Group | Count | Examples |
|---|---|---|
| Roles (generated) | 32 | --graphite-primary, --graphite-on-primary, --graphite-primary-container |
| States (generated) | 18 | --graphite-primary-hover, --graphite-primary-pressed, --graphite-primary-selected |
| Ladders (generated) | 6 | --graphite-elevation-00, --graphite-elevation-01, --graphite-elevation-02 |
| Focus and scrim (generated) | 2 | --graphite-focus, --graphite-scrim |
| space (static) | 14 | --graphite-space-00, --graphite-space-01, --graphite-space-02 |
| breakpoint (static) | 5 | --graphite-breakpoint-sm, --graphite-breakpoint-md, --graphite-breakpoint-lg |
| radius (static) | 8 | --graphite-radius-none, --graphite-radius-2, --graphite-radius-4 |
| density (static) | 3 | --graphite-density-compact, --graphite-density-default, --graphite-density-spacious |
| motion (static) | 5 | --graphite-motion-indeterminate, --graphite-motion-indeterminate-ease, --graphite-motion-ease |
| shadow (static) | 1 | --graphite-shadow-overlay |
| font (static) | 3 | --graphite-font-1, --graphite-font-2, --graphite-font-mono |
| text (static) | 62 | --graphite-text-weight-bold, --graphite-text-weight-medium, --graphite-text-weight-regular |
Generated variables come from your color, 58 per theme: thirty-two roles, six states for each of the primary, secondary, danger families, six ladder steps for layers and borders, focus and scrim. Static ones are the same for every theme. Tokens lists them all.
Step 6: Switch theme
Set data-theme to light or dark on <html>, or on any element to theme just that part.
'use client'
export function ThemeToggle() {
const toggle = () => {
const root = document.documentElement
root.dataset.theme = root.dataset.theme === 'dark' ? 'light' : 'dark'
}
return <button onClick={toggle}>Switch theme</button>
}Step 7: Add a component
Button follows its contract at 2.5.0, and ButtonGroup at 1.0.1. The CLI adds their files to components/ui, the helpers they import, and sass, clsx and class-variance-authority.
npx shadcn@latest add https://www.graphite-ui.com/r/button.json https://www.graphite-ui.com/r/button-group.jsonButton defaults to secondary, not the filled primary, and ButtonGroup allows one primary.
import { Button } from '@/components/ui/button'
import { ButtonGroup } from '@/components/ui/button-group'
export function SaveActions() {
return (
<ButtonGroup>
<Button>Cancel</Button>
<Button variant="primary">Save changes</Button>
</ButtonGroup>
)
}Next steps
Where to go once the basics work.
- Theming
How the roles are derived, and which to use where.
- Components
Every component, with its contract version.
- What you can use today
The theme, the Figma kit and the components, and their status.
