Skip to content

API reference

variants

The .variants() method chains onto a stylesheet to add conditional styling based on component state. Annotate the selector parameter with Variants<Mods>. Toned infers the returned rules and checks their parts, token values, and nested declarations.

A design-system module can re-export the Variants type. With a namespace import such as import * as ui from './ui', write $: ui.Variants<Mods>. The optional second callback parameter, q, infers the declared parts and system conditions. No currying or validation wrapper is needed. Older signatures remain available for compatibility.

Signature

TypeScript
import type { Variants } from '@toned/core'

const styles = stylesheet({ PartName: {} }).variants(($: Variants<{ variantName: 'value' }>) => ({
  [$.variantName('value')]: {
    PartName: { /* token overrides */ },
  },
}))

Defining Variants

Reuse ordinary TypeScript types for variant keys and their allowed values. A required axis without a default must be provided by the consumer; an optional axis (marked with ?) can be omitted. A rule matches when its selector values are selected:

TypeScript
import type { Variants } from '@toned/core'

type Size = 'm' | 's'
type ButtonVariants = {
  size: Size                 // reusable axis type
  variant: 'accent' | 'danger' // required
  alignment?: 'icon-only' | 'icon-left' | 'icon-right' // optional
}

const buttonStyles = stylesheet({
  Root: {
    $kind: 'pressable',
    borderRadius: 'medium',
    borderWidth: 'none',
    cursor: 'pointer',
  },
  Label: { $kind: 'text', typography: 'label-medium' },
}).variants(($: Variants<ButtonVariants>) => ({
  [$.variant('accent')]: {
    Root: { bgColor: 'action' },
    Label: { textColor: 'on_action' },
  },
  [$.variant('danger')]: {
    Root: { bgColor: 'destructive' },
    Label: { textColor: 'on_destructive' },
  },
  [$.size('m')]: {
    Root: { paddingX: 3 },
  },
  [$.size('s')]: {
    Root: { paddingX: 2, paddingY: 1 },
  },
}))

Defaults

Pass defaults as the second argument to give an axis the value it has when the component does not set one. An axis with a default becomes optional for the component; passing undefined selects the default too.

TypeScript
import type { Variants } from '@toned/core'

const tagStyles = stylesheet({
  Root: { borderRadius: 'medium' },
}).variants(($: Variants<{ size: 'm' | 's'; variant: 'accent' | 'danger' }>) => ({
  [$.size('m')]: { Root: { paddingX: 3 } },
  [$.size('s')]: { Root: { paddingX: 2 } },
  [$.variant('accent')]: { Root: { bgColor: 'action' } },
  [$.variant('danger')]: { Root: { bgColor: 'destructive' } },
}), { defaults: { size: 'm' } })

// size is optional and is 'm' when omitted; variant is still required.

The Dollar-Sign Builder

The callback receives a $ builder object with a method for each variant key. Calling $.size('m') produces a computed key that the runtime uses to match against the variant values a component selects. The keys it returns, and every other key a rule accepts, are listed in Conditions and selectors.

Compound Variants

Chain multiple variant calls to create compound conditions that only match when all specified variants are active simultaneously:

TypeScript
// Inside the .variants callback:
{
  [$.size('m').alignment('icon-only')]: {
    Root: { paddingX: 2, paddingY: 2 },
  },
  [$.size('s').alignment('icon-only')]: {
    Root: { paddingX: 1, paddingY: 1 },
  },
}

Matching variant rules apply in declaration order: later rules win conflicting properties. Chaining selectors adds conditions, not an automatic specificity bonus. Put a compound rule after the individual rules it should override.

Pseudo-state Variants

Colocate a state rule under the part it styles. Use ':hover' or the inferred [q.state('hover')] key inside that part:

TypeScript
[$.variant('accent')]: {
  Root: {
    bgColor: 'action',
    ':hover': { bgColor: 'action_secondary' },
  },
  Label: { textColor: 'on_action' },
}

Responsive Variants

Breakpoints work inside variant blocks, so a variant can define responsive overrides for its elements:

TypeScript
[$.layout('grid')]: {
  Root: {
    flexLayout: 'column',
    gap: 2,
    '@media md': {
      flexLayout: 'row',
      flexWrap: 'wrap',
    },
    '@media lg': {
      gap: 4,
    },
  },
}

Compound Queries Inside Variants

The second callback argument uses the same query builders as a base stylesheet. Combine media, state and platform conditions where the affected part is declared; no extra chaining method is needed.

TypeScript
import type { Variants } from '@toned/core'

const styles = stylesheet({ Root: { opacity: 1 } })
  .variants(($: Variants<{ variant: 'accent' | 'quiet' }>, q) => ({
    [$.variant('accent')]: {
      Root: {
        [q.all(q.media('md'), q.not(q.state('active')))]: {
          opacity: 0.75,
        },
        [q.any(q.state('hover'), q.state('focus'))]: {
          opacity: 1,
        },
      },
    },
  }))

Named Styles ($compose)

When multiple variants share common element overrides, you can extract them into a named style and compose them into variants. This avoids duplicating the same token values across variant rules.

Defining Named Styles

Use $('name') to define a named style block. Named styles are not variant rules — they are reusable fragments that can be composed into actual variants:

TypeScript
import type { Variants } from '@toned/core'

const buttonStyles = stylesheet({
  Root: { $kind: 'pressable', borderRadius: 'medium' },
  Label: { $kind: 'text' },
}).variants(($: Variants<{ size: 'm' | 's'; variant: 'accent' | 'danger' }>) => ({
  // Named style — shared across variants
  [$('interactive')]: {
    Root: { ':hover': { shadow: 'medium' } },
  },

  [$.variant('accent')]: {
    $compose: 'interactive',
    Root: { bgColor: 'action' },
    Label: { textColor: 'on_action' },
  },

  [$.variant('danger')]: {
    $compose: 'interactive',
    Root: { bgColor: 'destructive' },
    Label: { textColor: 'on_destructive' },
  },
}))

Both accent and danger inherit the hover shadow from interactive, without repeating it.

Named references autocomplete and reject misspellings. Composition is recursive: later entries in an array override earlier entries, then the consuming rule’s own properties win. Untyped declarations with unknown references or cycles throw when the stylesheet is constructed.

Element-Level $compose

Inside a variant rule, you can compose a declared part from another element defined in the same block. This is useful when several elements within a variant share a base set of tokens:

TypeScript
[$.size('s')]: {
  Base: { paddingX: 2, bgColor: 'muted' },
  Root: {
    $compose: 'Base',
    borderRadius: 'medium',
  },
  Sidebar: {
    $compose: 'Base',
    borderRadius: 'small',
  },
}

Here both Root and Sidebar inherit paddingX and bgColor from Base, then add their own overrides. The source (Base) must be a declared part. It remains available to render. A sibling definition in the same rule takes precedence over that part’s base declaration.

Composing Multiple Sources

Pass an array to $compose to merge from multiple named styles. They are applied in order, and the variant's own properties always take priority:

TypeScript
[$('borders')]: {
  Root: { borderWidth: 'thin', borderColor: 'subtle' },
},
[$('spacing')]: {
  Root: { paddingX: 3, paddingY: 2 },
},

[$.size('m')]: {
  $compose: ['borders', 'spacing'],
  Root: { bgColor: 'elevated' },  // own props override composed ones
}

Consuming Variants

Bind the sheet once with createElements and pass variant values to the family provider. Every part inside reads them from it. When you need prop bags instead, useStyles(buttonStyles, { size, variant }) takes the same values:

TypeScript
const S = createElements(buttonStyles)

function Button({ label, size, variant }: Props) {
  return (
    <S size={size} variant={variant}>
      <S.Root as="button" type="button">
        <S.Label as="span">{label}</S.Label>
      </S.Root>
    </S>
  )
}