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
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:
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.
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:
// 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:
[$.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:
[$.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.
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:
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:
[$.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:
[$('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:
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>
)
}