Skip to main content
Graphite UI

Quick start

Graphite in your own project, from a color to a themed screen.

Next.js App RouterInstalls with the shadcn CLI

Before you start

  • Node 20 or newer.
  • An existing Next.js project on the App Router, in TypeScript. npx create-next-app@latest gives you one.
  • The @/ import alias, which create-next-app sets 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.json

It 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.json

Step 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-mono

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

Live: change the source and this follows
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);
}
The --graphite-* variables in a theme file
GroupCountExamples
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.json

Button defaults to secondary, not the filled primary, and ButtonGroup allows one primary.

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