Get started
One package, one stylesheet, and every token is yours to override
The design system ships as @robr0/design-system, the same package this site is built with. There is no configuration API or theme provider: theming is plain CSS custom properties. Import the token stylesheet, use the components, and re-theme by redefining tokens. (The one provider in the library is ToastProvider, needed only if you use the toast queue via useToast.) Try it live in the playground: it generates the exact CSS you would paste into your app.
Install
React 19+ (react and react-dom) is the only required peer dependency (recharts is an optional extra, for charts). Everything else (component CSS, both themes, the Material Symbols icon font) is bundled. The package 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").
npm install @robr0/design-system// Load the tokens once — primitives, semantic tokens, and both themes.
import '@robr0/design-system/tokens/tokens.css';
// Then import components — from the barrel…
import { Button, Card, Badge } from '@robr0/design-system';
// …or by deep path (what this site does):
import { Button } from '@robr0/design-system/components/Button/Button';
// Optional: only if you render raw .material-symbols-rounded spans —
// any component import already loads the icon font for you.
import '@robr0/design-system/fonts/material-symbols.css';// Charts live behind their own entry so the recharts peer
// dependency stays optional — install recharts only if you use them.
import { BarChart, LineChart } from '@robr0/design-system/charts';Dark mode
Every semantic token has a light and a dark value. Set data-theme="dark" on the root element to switch. There are no prefers-color-scheme queries in components, so your app decides when.
<!-- Light is the default; flip the whole system with one attribute -->
<html data-theme="dark">Bring your own font
The system is designed for Nunito Sans but deliberately does not bundle it. Load it (or any font) however your stack prefers and point one token at it. This site loads Nunito Sans with next/font and does exactly this override in its global CSS.
/* 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;
}Re-theme with primitives
Tokens are three tiers: primitives hold the raw values, semantic tokens reference primitives, components use semantic tokens. That chain is build-enforced, which is what makes a primitive override cascade through the entire system, both themes included.
/* Every semantic token references a primitive, so overriding a
primitive re-themes everything built on it — in both themes. */
:root {
/* Your brand colour becomes the action colour (buttons, focus rings) */
--primitive-teal-07: #7C3AED;
/* Its pressed/hover ramp neighbours */
--primitive-teal-08: #6D31D3;
--primitive-teal-09: #4C2293;
/* Pill buttons become rounded rectangles */
--primitive-radius-full: 12px;
}Semantic tokens are fair game too when you want to change one meaning without touching the ramp it comes from:
/* 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;
}Chat and agent UI
The ai category installs with the rest of the package: chat surface primitives (Chat thread, Chat message, Composer, Chat header), agent-state components (Agent status, Reasoning, Tool call), and supporting pieces such as Prose and the citation chips. They are components like any other here, themed by the same tokens, and they render whatever conversation you hand them.
What the package does not ship is the conversation itself. You bring the state (the transcript, which turn is streaming), a transport that talks to your backend, and a server-side endpoint holding your LLM API key. Keys stay on the server; nothing in the package or your client code ever holds one. This site's chat is the reference implementation: and you are looking at those components at work.
Ambient background
Shader field is the one component that asks more of you than an import. It renders a WebGL2 field of soft light sources that read your colour tokens at runtime, so it re-themes with everything else. But it fills a positioned ancestor you provide, and it can fail on hardware you do not control. So it never decides what to paint instead of itself: it reports pending, active or unavailable, and one fallback covers every failure. It also checks prefers-reduced-motion itself, since the CSS motion tokens cannot see an animation loop.
import { ShaderField, type ShaderFieldStatus } from '@robr0/design-system';
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>The background behind this page is that component, with eight blurred CSS discs kept painted underneath as its fallback.
See it live
The playground applies these overrides to a full page in real time (navigation, components, and the chat widget): pick a brand colour, tint the neutrals, reshape the radii, swap the font, then copy the generated CSS.