Typed styling for design systems
Define your tokens once. Toned builds them into styles for the web, React Native, email and PDF, and TypeScript checks every value.
Also supported
- OutputWeb CSS, built ahead of time
- OutputSSR and static HTML
- BindingReact Server Components
- BackendTailwind
- BuildVite plugin
- TokensDTCG import and export
- EditorLanguage server
- EditorVS Code extension
- LintESLint and Oxlint rules
- CoreOther frameworksthrough the core renderer; no binding package
Build time
CSS is generated when you build. Nothing is injected at render.
SSR and Server Components
Server and static pages need no style runtime, and Server Components resolve styles without hooks.
Cross-platform
One stylesheet resolves for the web, React Native, email and PDF.
Design system first
You define the system. Components can only use what it defines.
Token first
Every value is a named token, from colour and type to layout.
Type safe
Tokens, parts and variants are typed. A wrong value does not compile.
Themes
One implementation, infinite customisation
One set of components, 6 themes. Pick one: colour, type, borders, corners and shadows all change, and the code does not.
Soft neutrals, underlined tabs, rounded corners, light shadows.
Written once
The stylesheets name roles such as fill: ‘accent’. None of them names a theme.
A theme is one typed object
Every theme satisfies the same type, so a missing or unknown field does not compile.
Switching is CSS only
Choosing a theme sets one data-theme attribute. No component rerenders and no class changes.
How it works
Tokens in, components out
Follow one small component from design tokens to the screen. This is the code the page runs: change the variant and the result follows.
Live result
It starts when a runner is free.
<Notice tone="info" size="regular" />Define your tokens
A token is a design decision with a name: which tints exist, which shapes, which text styles. Together they are the vocabulary your components are written in.
import { defineSystem, defineToken } from '@toned/core'
const colours = {
info: { soft: '#eef2ff', line: '#bac8ff', solid: '#284bdd' },
success: { soft: '#eaf6ef', line: '#cde9d8', solid: '#1d7a4c' },
danger: { soft: '#fdecea', line: '#f3c9c4', solid: '#b3261e' },
} as const
const shapes = {
card: { padding: 20, borderRadius: 12 },
compact: { padding: 12, borderRadius: 12 },
pill: { padding: '2px 10px', borderRadius: 999 },
} as const
const text = {
title: { fontSize: 18, fontWeight: 600, color: '#17234b' },
body: { fontSize: 15, fontWeight: 400, color: '#34405f' },
label: { fontSize: 12, fontWeight: 600, color: '#ffffff' },
} as const
type Tone = keyof typeof colours
// A token is one design decision: its name, the values it allows, and what
// each value means. A stylesheet can use these values and nothing else.
export const ui = defineSystem({
id: 'notice',
tokens: {
tint: defineToken({
values: ['info', 'success', 'danger'],
resolve: (tone: Tone) => ({
backgroundColor: colours[tone].soft,
borderColor: colours[tone].line,
borderWidth: 1,
borderStyle: 'solid',
}),
}),
fill: defineToken({
values: ['info', 'success', 'danger'],
resolve: (tone: Tone) => ({ backgroundColor: colours[tone].solid }),
}),
shape: defineToken({
values: ['card', 'compact', 'pill'],
resolve: (shape: keyof typeof shapes) => shapes[shape],
}),
text: defineToken({
values: ['title', 'body', 'label'],
resolve: (style: keyof typeof text) => text[style],
}),
stack: defineToken({
values: [4, 8] as const,
resolve: (gap) => ({
display: 'flex',
flexDirection: 'column',
alignItems: 'flex-start',
gap,
}),
}),
},
})
export const { stylesheet } = uiStyle the parts
Name the component’s parts and give each one token values. Variants sit beside them: a tone changes the root and the badge together.
import type { Variants } from '@toned/core'
import { stylesheet } from './system.ts'
type NoticeVariants = {
tone: 'info' | 'success' | 'danger'
size: 'regular' | 'compact'
}
export const noticeStyles = stylesheet({
Root: { tint: 'info', shape: 'card', stack: 8 },
Badge: { $kind: 'text', fill: 'info', shape: 'pill', text: 'label' },
Title: { $kind: 'text', text: 'title' },
Body: { $kind: 'text', text: 'body' },
}).variants(($: Variants<NoticeVariants>) => ({
[$.tone('success')]: {
Root: { tint: 'success' },
Badge: { fill: 'success' },
},
[$.tone('danger')]: {
Root: { tint: 'danger' },
Badge: { fill: 'danger' },
},
[$.size('compact')]: {
Root: { shape: 'compact', stack: 4 },
Title: { text: 'body' },
},
}))Your editor completes the values, and a typo does not compile:
Root: { tint: 'sucess', shape: 'card', stack: 8 },Use it in a component
createElements binds the parts to elements. The component passes the variants as props and holds no class names or style logic.
import { createElements } from '@toned/react'
import type { ReactNode } from 'react'
import { noticeStyles } from './styles.ts'
const Parts = createElements(noticeStyles)
export function Notice({
tone,
size,
label,
title,
children,
}: {
tone: 'info' | 'success' | 'danger'
size: 'regular' | 'compact'
label: string
title: string
children: ReactNode
}) {
return (
<Parts tone={tone} size={size}>
<Parts.Root>
<Parts.Badge as="span">{label}</Parts.Badge>
<Parts.Title as="strong">{title}</Parts.Title>
<Parts.Body as="p">{children}</Parts.Body>
</Parts.Root>
</Parts>
)
}What comes out
On the web the CSS is built ahead of time and each part gets class names. The same stylesheet also resolves to inline styles for HTML email. These are the root’s props for the variant you picked.
import { createInlineRenderer } from '@toned/core/server'
import { noticeStyles } from './styles.ts'
import { ui } from './system.ts'
// No React and no stylesheet: the core resolves the sheet to plain props.
const email = createInlineRenderer(ui, { tokens: {} })
export function noticeEmailProps(variants: {
tone: 'info' | 'success' | 'danger'
size: 'regular' | 'compact'
}) {
return email.resolve(noticeStyles, { variants })
}{
"className": "_ notice--tint_info notice--shape_card notice--stack_8",
"style": {}
}{
"style": {
"backgroundColor": "#eef2ff",
"borderTopColor": "#bac8ff",
"borderRightColor": "#bac8ff",
"borderBottomColor": "#bac8ff",
"borderLeftColor": "#bac8ff",
"borderTopWidth": 1,
"borderRightWidth": 1,
"borderBottomWidth": 1,
"borderLeftWidth": 1,
"borderStyle": "solid",
"paddingTop": 20,
"paddingRight": 20,
"paddingBottom": 20,
"paddingLeft": 20,
"borderRadius": 12,
"display": "flex",
"flexDirection": "column",
"alignItems": "flex-start",
"gap": 8
}
}Developer experience
Your editor knows the design system
The language server, the compiler and the lint rules all read the tokens you defined. Hover a case to see what each one reports.
- info
- ui.tint
- success
- ui.tint
- danger
- ui.tint
Also included
More in the box
Named parts
A stylesheet styles a component’s root, label and action together, and binds them to elements.
stylesheetVariants
Sizes, tones, states and their combinations are declared in the sheet, with typed selectors.
variantsConditions
Media queries, container queries, interaction states and cross-part relationships share one typed query builder.
ConditionsTailwind backend
Resolve fields to utilities from your own Tailwind build, validated against the declarations they emit.
Backend integrationsDesign-token exchange
Import and export a documented subset of the DTCG format, with diagnostics for what is not supported.
DTCG token exchangeMeasured contracts
Enumerate variants and themes, then check rendered geometry and contrast against rules you declare.
Design contractsWhere next
Start with the guide, or look around
Getting started
Install the packages, define a system, write a stylesheet and render it, with the build setup for Vite.