Skip to content
Toned

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.

Slipway

Web app

Stable

Frankfurt · 4 releases

Deploys this week
24
+6vs last week
Success rate
98.2%
+0.4points, 30 days
Median build
3m 12s
+18svs last week
VersionBranchStatusOwnerRollout
4.12.0mainRolling out
Ada King
60%
4.11.2hotfix/loginLive
Mo Rahimi
100%
4.11.1mainFailed
Jun Lee
20%
4.11.0mainLive
Ada King
100%

New release

It is added to the top of the table.
Start the rollout at

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.

See the themes and their source

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

QueuedRelease 4.12 is queued

It starts when a runner is free.

Tone
Size
<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.

system.ts
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 } = ui

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

styles.ts
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 },
error TS2820: Type '"sucess"' is not assignable to type '"danger" | "success" | "info" | undefined'. Did you mean '"success"'?

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.

Notice.tsx
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.

email.ts
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 })
}
Web: class names
{
  "className": "_ notice--tint_info notice--shape_card notice--stack_8",
  "style": {}
}
Email: inline styles
{
  "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
  }
}
Set this up in a project

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.

styles.tsCompletion offers the values a token allows, and only those.
export const noticeStyles = stylesheet({
Root: { tint: '', shape: 'card', stack: 8 },
info
ui.tint
success
ui.tint
danger
ui.tint
Badge: { $kind: 'text', fill: 'info', shape: 'pill', text: 'label' },

Also included

More in the box

Named parts

A stylesheet styles a component’s root, label and action together, and binds them to elements.

stylesheet

Variants

Sizes, tones, states and their combinations are declared in the sheet, with typed selectors.

variants

Conditions

Media queries, container queries, interaction states and cross-part relationships share one typed query builder.

Conditions

Tailwind backend

Resolve fields to utilities from your own Tailwind build, validated against the declarations they emit.

Backend integrations

Design-token exchange

Import and export a documented subset of the DTCG format, with diagnostics for what is not supported.

DTCG token exchange

Measured contracts

Enumerate variants and themes, then check rendered geometry and contrast against rules you declare.

Design contracts
All references

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

Read the guide