Get started
Install Rift DS, then make it yours
rift-ds is the package this site is built with. Theming is plain CSS custom properties, with no configuration API or theme provider. Try it in the playground.
Install
Use this if a coding agent writes your code. Paste this file into your agent, or save it at your project root as AGENTS.md. Your agent does the install from there.
# Building with rift-ds
You are building React UI with rift-ds 1.25.0. Follow this file before writing any code with it. It was generated from the library's registries, so every name in it exists.
## Start a Next.js app
In a Next.js app router project (a fresh `create-next-app` is one), three commands give a themed first page:
```bash
npm install rift-ds
npx rift-ds init app
npx rift-ds init block dashboard
```
1. `init app` writes `components/rift/`: the wrapper that routes every link through Next's `Link`, the script that applies the stored colour mode before first paint, and the base theme's font. It prints the root layout to use. Make `app/layout.tsx` exactly that, keeping its metadata, and drop any `globals.css` import `create-next-app` left there: its body colours and font cover the library's.
2. `init block <name>` copies in a whole starting page built from the library, laid out for desktop and phone: `app-shell`, `dashboard` and `sign-in`. With no name it lists them. The files are the project's own from then on, so change them freely.
3. Start every new screen from a block, or inside the `AppShell` the app shell and dashboard blocks write to `components/blocks/`, never from a blank file, then fill it with library components.
The init commands only add files and never overwrite one, so they are safe to run again.
## Install anywhere else
```bash
npm install rift-ds
```
- Required peers: `react` ^19.0.0 and `react-dom` ^19.0.0. It brings its own runtime dependencies, `@tanstack/react-table` ^9.3.1 and `@tanstack/react-virtual` ^3.14.15 (the engine and the windowing under `DataTable`), which install with it, so add nothing else for them.
- `recharts` ^3.0.0 is optional. Install it only if you import from `rift-ds/charts`.
- It is ESM-only. Use a bundler that handles CSS and font imports (Vite, Next.js, webpack), with TypeScript's `moduleResolution` set to `"bundler"` or `"nodenext"`.
## Setup by hand
What `init app` does, for an app it does not cover:
```tsx
import 'rift-ds/tokens/tokens.css'; // once, at the app root
import 'rift-ds/tokens/base.css'; // the reset and page the components are tuned against
import { Button } from 'rift-ds';
```
- `base.css` paints the page (`body` on `--color-bg-page-primary`, `--color-text-primary` and `--font-family-primary`) and resets box sizing and margins; without it, components render too padded.
- The base theme's typeface is not bundled. It is designed around Nunito Sans: load that (or your own) and set `--font-family-primary` to it. A shipped theme bundles its own faces, and the icon font always ships.
- Dark mode is `data-theme="dark"` on `<html>`. `colorModeScript()` from `rift-ds/components/ThemeToggle/colorMode` applies the stored choice before first paint, and `ThemeToggle` changes it. A shipped theme is `data-brand` on `<html>`, with `rift-ds/tokens/presets/presets.css` imported once.
- In a routed app, wrap the tree in `LinkProvider` from `rift-ds/behaviors/LinkProvider` with the router's link, or every component link reloads the page. A component rendered by a Server Component cannot read it, so pass `linkComponent` on that instance instead.
- For an app not in US English, wrap the tree in `LocaleProvider` from `rift-ds/behaviors/LocaleProvider` with its locale, or every number, date and calendar reads as `en-US`. A component rendered by a Server Component cannot read it, so pass `locale` on that instance instead. Translate the words through each component's label props.
- Bind keyboard shortcuts with `useShortcut` from `rift-ds/behaviors/shortcuts` (`mod+k` is Cmd+K on a Mac and Ctrl+K elsewhere), never a document keydown listener: it is the listener the components use, so a binding stays quiet in text fields and never takes Escape from an open overlay. Draw a shortcut with Kbd's `keys` prop, not hand-typed ⌘ glyphs.
- For a right-to-left language set `dir="rtl"` on `<html>` and the components mirror themselves. In your own CSS use logical properties (`margin-inline-start`, not `margin-left`), and pass `start`/`end`, not `left`/`right`, to Drawer's `side`, popup positions and column `align`, unless the side belongs to the screen.
- Wrap the tree in `ToastProvider` only if you call `useToast` (both from the `Toast` module). No other component needs a provider.
## AI chat
Do not build a chat UI by hand. `npx rift-ds init chat` scaffolds the package's chat widget into a Next.js app router project: the routes, the mount and an env example. It never overwrites a file, and it ends by printing the optional peers to install and the steps to wire it in. Follow that output. In any other framework, use the panel over your own endpoint or the headless engine instead: https://rift-ds.com/docs/chat covers both and the protocol, and every prop is on https://rift-ds.com/components/ai-chat.
## Rules
- Use a library component before building your own, and never restyle one from outside. If it looks wrong, change a token.
- Never write a raw colour, spacing, radius, font value or duration. Use the semantic token for the role, not the nearest value.
- Never write a token name from memory. Check it in the token reference below, or read the names from `tokenRegistry` in `rift-ds/tokens/registry`: a misspelt `var()` resolves to nothing and fails without a sound.
- `--color-action-primary-bg` marks the primary action, focus, and checked or selected state. Never decoration.
- Status colours come from the `--color-status-*` family, such as `--color-status-error-text`.
- The only shadows are `--shadow-floating`, `--shadow-modal` and `--shadow-table-pinned`.
- Size an icon by setting `--icon-size` to an `--icon-size-*` step, never `font-size`.
- Never guess a prop. Check it in the `.d.ts` files in `node_modules/rift-ds`, which work offline, or in the component's contract, linked from the catalogue below.
## Look it up
- Every component, with its import and a link to its prop contract: https://rift-ds.com/skill/rift-design-system/references/components.md
- Every token, with its value in light and dark: https://rift-ds.com/skill/rift-design-system/references/tokens.md
- Every component's contract as JSON: https://rift-ds.com/schema/components/<slug>.json
- Every token resolved per theme, in code: `rift-ds/tokens/values`
- MCP endpoint, no key needed: https://rift-ds.com/api/mcp
- If the project uses `.claude/skills/`, `npx rift-ds init` installs the full skills there.
Optional: give your agent more
Connect the MCP endpoint so it can look up exact props and tokens while it builds. No key or account is needed.
claude mcp add --transport http rift-ds https://rift-ds.com/api/mcpInstall the skills it loads every session. This also prints the MCP connect line.
npx rift-ds init{
"mcpServers": {
"rift-ds": { "url": "https://rift-ds.com/api/mcp" }
}
}{
"servers": {
"rift-ds": { "type": "http", "url": "https://rift-ds.com/api/mcp" }
}
}- “Which components are in the ai category?”
list_components - “What props does DataTable take?”
get_component - “Which semantic tokens control motion?”
list_tokens - “What does this site say about design tokens?”
search_site - “How do I install the library and set up dark mode?”
get_setup
The same contracts are plain files too: every component page's For agents button has its markdown and its JSON schema, and Schemas covers the rest.
- Install, theming and dark mode rules, the component catalogue and the token reference.
rift-design-system - Restyles existing screens onto the tokens and components.
rift-apply-theme - Audits a project's styles for hardcoded values that should be tokens.
rift-style-audit
npx rift-ds init writes them to .claude/skills/. Add --skill and a name to install one, and re-run it to refresh them. No npm nearby? The same files are one curl each:
curl --create-dirs -o .claude/skills/rift-design-system/SKILL.md https://rift-ds.com/skill/rift-design-system/SKILL.md
curl --create-dirs -o .claude/skills/rift-design-system/references/components.md https://rift-ds.com/skill/rift-design-system/references/components.md
curl --create-dirs -o .claude/skills/rift-design-system/references/tokens.md https://rift-ds.com/skill/rift-design-system/references/tokens.md
curl --create-dirs -o .claude/skills/rift-apply-theme/SKILL.md https://rift-ds.com/skill/rift-apply-theme/SKILL.md
curl --create-dirs -o .claude/skills/rift-style-audit/SKILL.md https://rift-ds.com/skill/rift-style-audit/SKILL.mdFetch it again after you upgrade:
curl -o AGENTS.md https://rift-ds.com/AGENTS.mdMade a theme in the playground? Its Get started button gives you one AGENTS.md with the setup, the theme in words and its CSS.
Use this if you have a Next.js app router project, or a fresh create-next-app. Run these:
npm install rift-ds
npx rift-ds init app
npx rift-ds init block dashboardThen make app/layout.tsx this, keep your metadata, and drop the globals.css import create-next-app added.
// app/layout.tsx
import 'rift-ds/tokens/tokens.css';
import 'rift-ds/tokens/base.css';
import '../components/rift/font.css';
import { riftFont } from '../components/rift/font';
import { ColorModeScript } from '../components/rift/ColorModeScript';
import { RiftRoot } from '../components/rift/RiftRoot';
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en" className={riftFont.variable} suppressHydrationWarning>
<head>
<ColorModeScript />
</head>
<body>
<RiftRoot>{children}</RiftRoot>
</body>
</html>
);
}Your first page is at /dashboard. Neither init command overwrites a file. Blocks lists the other starting pages.
Use this for any other React 19 app with a bundler, such as Vite.
npm install rift-ds// Load the tokens once — primitives, semantic tokens, and both themes.
import 'rift-ds/tokens/tokens.css';
// The reset and the page every component is tuned against
// (border-box sizing, no stray margins, the body's font and colours).
// Leave it out only if your app already styles the page.
import 'rift-ds/tokens/base.css';
// Then import components — from the barrel…
import { Button, Card, Badge } from 'rift-ds';
// …or by deep path (what this site does):
import { Button } from 'rift-ds/components/Button/Button';
// Optional: only if you render raw .material-symbols-rounded spans —
// any component import already loads the icon font for you.
import 'rift-ds/fonts/material-symbols.css';React 19 or later and a bundler that handles CSS and font imports, with TypeScript's moduleResolution set to "bundler" or "nodenext". The package is ESM-only.
Recharts is an optional peer, needed only for the recharts-backed charts.
// The recharts-backed charts live behind their own entry so that peer
// dependency stays optional — the dependency-free chart pieces
// (ContributionGraph, GanttChart, Gauge, LegendTile, Sparkline) come from the main barrel.
import { BarChart, LineChart } from 'rift-ds/charts';ToastProvider for useToast, LinkProvider for your router's link, and LocaleProvider for the locale numbers and dates are written in.
Single components
Use this if you want source you own, not a dependency. The shadcn CLI brings the component, what it builds on and the shared base. /r/registry.json lists them all. If your project has no components.json, run npx shadcn init first.
npx shadcn@latest add https://rift-ds.com/r/button.jsonThe whole repo
Use this to run or fork the system itself. Everything is MIT licensed, this site included, and one install brings up both Storybook and these docs.
git clone https://github.com/robritacca-dotcom/rift-ds.git
cd rift-ds
npm install # one install; the website is an npm workspace
npm run storybook # the component sandbox, or:
npm run dev -w website # this whole docs site, locallyCustomise
Theme
Components read semantic tokens, and semantic tokens read primitives, so overriding a primitive changes everything built on it, in both modes. The playground writes this CSS for you.
/* Every semantic token references a primitive, so overriding a
primitive re-themes everything built on it — in both themes.
In the base token files the action colour runs on the teal ramp:
light fills on teal-08/09/10, dark on teal-05/04/03. Re-key those
steps to rebrand —
or copy a complete override from the playground. */
:root {
--primitive-teal-08: #6D31D3; /* light fill */
--primitive-teal-09: #4C2293; /* light hover */
--primitive-teal-10: #2E1560; /* light active, dark label */
--primitive-teal-05: #A78BFA; /* dark fill */
--primitive-teal-04: #C4B5FD; /* dark hover */
--primitive-teal-03: #DDD6FE; /* dark active */
/* Pill buttons become rounded rectangles */
--primitive-radius-pill: 12px;
}To change one meaning, override its semantic token:
/* Prefer surgical changes? Override a semantic token directly —
scope the dark value under the theme attribute. */
:root {
--color-status-info-border: #345AC4;
}
[data-theme="dark"] {
--color-status-info-border: #7F99E3;
}Shipped themes
Import the aggregate once and set one attribute: Smoke, Getaway, Ember, Signal, Volt, Marquee, Zest, Forest, Terminal, Blueprint, Velvet, Bubblegum. Each also loads alone from tokens/presets/<id>.css.
// Every shipped theme, one generated stylesheet each, plus this aggregate.
import 'rift-ds/tokens/presets/presets.css';
<html data-brand="terminal">Dark mode
Set data-theme="dark" on the root element. Components have no prefers-color-scheme queries, so your app decides when, and base.css brings the page and native controls along.
<!-- Light is the default; flip the whole system with one attribute -->
<html data-theme="dark">To let visitors choose, apply the stored mode before first paint and add a ThemeToggle:
// app/layout.tsx: apply the visitor's stored mode before first paint.
import { colorModeScript } from 'rift-ds/components/ThemeToggle/colorMode';
<html lang="en" suppressHydrationWarning>
<head>
<script dangerouslySetInnerHTML={{ __html: colorModeScript() }} />
</head>
…
</html>
// Anywhere the mode should change: System, Light and Dark.
import { ThemeToggle } from 'rift-ds/components/ThemeToggle/ThemeToggle';
<ThemeToggle />Fonts
No text face is bundled. Load any font and point the family token at it, or split headings and body into two roles.
/* The whole type scale chains to one token.
Load any font (Google Fonts, next/font, self-hosted), then: */
:root {
--font-family-primary: 'Inter', sans-serif;
}
/* Or mix faces: display and heading styles read one family role,
body styles read the other. Both default to the primary family,
so overriding either role alone leaves the rest untouched. */
:root {
--font-family-heading: 'Fraunces', serif;
--font-family-body: 'Inter', sans-serif;
}Icons
Material Symbols ships as a variable font, with fill, weight and grade as custom properties at any scope. Every icon prop also takes your own element.
/* The bundled Material Symbols font keeps every Google axis live. */
:root {
--material-symbols-fill: 1; /* 0 line · 1 filled */
--material-symbols-weight: 300; /* 100-700 stroke thickness */
--material-symbols-grade: 0; /* -50-200 contrast tuning */
}// Every icon prop takes a Material name or your own element.
import { Search } from 'lucide-react';
<Input iconLeft="search" />
<Input iconLeft={<Search size={20} />} />Locale and direction
Numbers, dates and times are written in US English until you wrap the app in LocaleProvider. A component's own locale prop wins, and the few that render on the server take the prop alone.
// app/providers.tsx: a client component, mounted from the root layout.
'use client';
import { LocaleProvider } from 'rift-ds/behaviors/LocaleProvider';
export function Providers({ children }: { children: React.ReactNode }) {
return <LocaleProvider locale="de-DE">{children}</LocaleProvider>;
}The locale formats and never translates: every word a component writes is a prop with an English default. For a right-to-left language, set dir="rtl" on <html> and the layout, keys, popups and directional icons follow.
Money and dates
An amount is a decimal string such as "1234.50", never a float. rift-ds/format reads what a person typed in their locale, adds and multiplies on whole numbers, and writes money and relative dates. It has no React, so a server route can use it too.
import { addDecimal, formatMoney, formatRelative, parseDecimal } from 'rift-ds/format';
const typed = parseDecimal('1.234,5', { locale: 'de-DE' }); // "1234.5"
const total = addDecimal(typed ?? '0', '0.10'); // "1234.60", never a float
formatMoney(total, { currency: 'EUR', locale: 'de-DE' }); // "1.234,60 €"
formatRelative('2026-11-09', { now: '2026-11-06', minUnit: 'day' }); // "in 3 days"Keyboard shortcuts
useShortcut binds a key string, mod+s being ⌘S on a Mac and Ctrl+S elsewhere. It stays quiet while someone types and never takes Escape from an open overlay. Give it a description and useRegisteredShortcuts lists it for a help dialog.
'use client';
import { useShortcut } from 'rift-ds/behaviors/shortcuts';
import { Kbd } from 'rift-ds/components/Kbd/Kbd';
export function SaveButton({ onSave }: { onSave: () => void }) {
useShortcut('mod+s', onSave, { description: 'Save', allowInInput: true });
return <button onClick={onSave}>Save <Kbd keys="mod+s" size="compact" /></button>;
}Forms
Give a control a name and it submits with the form around it, so FormData and a server action read it with no state of your own. Custom controls submit through a hidden input, which native validation skips, so check required where you handle the submit.
import { Checkbox } from 'rift-ds/components/Checkbox/Checkbox';
import { Dropdown } from 'rift-ds/components/Dropdown/Dropdown';
import { TagInput } from 'rift-ds/components/TagInput/TagInput';
// FormData reads: plan=pro, tag=design, tag=docs, terms=on
<form action={saveProject}>
<Dropdown name="plan" label="Plan" defaultValue="pro" options={plans} />
<TagInput name="tag" label="Tags" defaultValues={['design', 'docs']} />
<Checkbox name="terms" label="Accept the terms" defaultChecked />
<button type="submit">Save</button>
</form>Chat
One command scaffolds the chat widget into a Next.js app, with its server handlers, and a scripted mode runs it with no key. Keys stay on the server. This site's is that widget. Its component page has every option.
npx rift-ds init chatAmbient background
Shader field fills a positioned ancestor you provide and can fail on hardware you do not control, so it reports its status and you paint the fallback.
import { ShaderField, type ShaderFieldStatus } from 'rift-ds';
const [status, setStatus] = useState<ShaderFieldStatus>('pending');
// Note: the fallback paints on 'unavailable', not on 'not active'.
<div style={{ position: 'fixed', inset: 0, zIndex: -1 }}>
{status === 'unavailable' && <YourCssFallback />}
<ShaderField params={{ streak: 0.4 }} onStatusChange={setStatus} />
</div>