Skip to content

Get started

Getting Started

Toned combines typed design tokens, named parts and variants. React element families keep component identities stable while each mounted instance owns its styling state. Web styles are generated before rendering.

Install

@toned/core holds the system, stylesheets and the CSS build; @toned/react binds them to React 18 or 19.

Terminal
npm install @toned/core @toned/react

Three more packages are optional: @toned/systems (a ready-made token vocabulary), @toned/themes (theme values for that vocabulary) and, as development dependencies, @toned/eslint-plugin and @toned/compiler (lint rules, the language server and the design tooling). The changelog lists what each release contains.

Declare a stylesheet

A stylesheet names the parts of a component, gives each part token values, and describes how they change per variant. It imports stylesheet from the system module defined further down.

styles.ts
import type { Variants } from '@toned/core'
import { stylesheet } from './system.ts'

export const buttonStyles = stylesheet({
  Root: { $kind: 'pressable', background: 'accent' },
  Label: { $kind: 'text', text: 'label' },
}).variants(($: Variants<{ size: 's' | 'm' }>) => ({
  [$.size('s')]: { Root: { padding: 1 }, Label: { text: 'caption' } },
  [$.size('m')]: { Root: { padding: 2 } },
}), { defaults: { size: 'm' } })

Render the parts

Bind the sheet once, at module scope. Variant values go on the family provider; host props go on the parts.

Button.tsx
import { createElements } from '@toned/react'
import { buttonStyles } from './styles.ts'

const S = createElements(buttonStyles)

export function Button({ label, size = 'm' }: {
  label: string
  size?: 's' | 'm'
}) {
  return (
    <S size={size}>
      <S.Root as="button" type="button">
        <S.Label as="span">{label}</S.Label>
      </S.Root>
    </S>
  )
}

Define the system

The system holds the tokens the sheet used above. Keep it, like the sheets, in a pure module that both the build and the application import.

system.ts
import { defineSystem, defineToken } from '@toned/core'

export const ui = defineSystem({
  id: 'example',
  tokens: {
    background: defineToken({
      values: ['neutral', 'accent'] as const,
      resolve: value => ({ backgroundColor: value === 'accent' ? '#315bd6' : '#eee' }),
    }),
    padding: defineToken({
      values: [1, 2, 3] as const,
      resolve: step => ({ padding: step * 4 }),
    }),
    text: defineToken({
      values: ['label', 'caption'] as const,
      resolve: value => ({ color: '#fff', fontSize: value === 'label' ? 14 : 12 }),
    }),
  },
})

export const { stylesheet } = ui

Build every sheet

The Vite plugin emits static CSS and a matching manifest. Include lazy-route sheets in the inventory too; rendering does not discover missing CSS.

vite.config.ts
import toned from '@toned/core/vite'
import react from '@vitejs/plugin-react'
import { defineConfig } from 'vite'
import { buttonStyles } from './styles.ts'
import { ui } from './system.ts'

export default defineConfig({
  plugins: [
    toned({ system: ui, sheets: [buttonStyles], inputs: ['system.ts', 'styles.ts'] }),
    react(),
  ],
})

Without Vite, call buildStyles(ui, { sheets }) from @toned/core/build in your build script and serve its CSS and manifest together.

Configure and render

The application creates one renderer from the system and the generated manifest, and provides it to the tree.

App.tsx
import 'virtual:toned.css'
import manifest from 'virtual:toned.manifest'
import { createWebRenderer } from '@toned/core/server'
import { TonedProvider } from '@toned/react'
import { webHost } from '@toned/react/hosts/web'
import { Button } from './Button.tsx'
import { ui } from './system.ts'

const renderer = createWebRenderer(ui, { manifest })

export function App() {
  return (
    <TonedProvider renderer={renderer} host={webHost}>
      <Button label="Save" size="s" />
    </TonedProvider>
  )
}
env.d.ts
/// <reference types="vite/client" />
declare module 'virtual:toned.css' {}
declare module 'virtual:toned.manifest' {
  import type { BuildManifest } from '@toned/core/build'
  const manifest: BuildManifest
  export default manifest
}

The family provider adds no DOM element. Independent parts can render standalone with base/default styles; use the provider for variant inputs, cross-part states, and grid ownership. Sibling providers remain isolated.

useStyles prop bags and the existing useBind API remain supported. See the React bindings guide for their contracts.