useStyles
useStyles is the React hook that connects a toned stylesheet to your component. It resolves tokens into platform-appropriate props (style, className, etc.) and applies variant matching based on the state you provide.
Signature
import { useStyles } from '@toned/react'
import { buttonStyles, cardStyles } from './styles.ts'
// Without variants
const card = useStyles(cardStyles)
// With variants
const button = useStyles(buttonStyles, { variant: 'accent', size: 'm' })The examples on this page read their sheets from one module, shown at the end.
Parameters
stylesheet -- A stylesheet created with stylesheet(), optionally with .variants() chained. This is the style definition to resolve.
state -- An object of variant key-value pairs. Required when the stylesheet has required variants; omit when there are no variants. TypeScript enforces correctness here.
Return Value
An object with one key per element defined in the stylesheet. Each value is a props object that can be spread onto a JSX element:
const s = useStyles(buttonStyles, { size: 'm', variant: 'accent' })
// s.Root => { style: {...}, className: '...' }
// s.Label => { style: {...}, className: '...' }
return (
<button {...s.Root}>
<span {...s.Label}>Click me</span>
</button>
)How It Works
Each render creates a private candidate using the current configuration, tokens, overrides and variants. The candidate becomes committed in a layout effect after host mutations. Suspended renders cannot publish pending inputs. Committed interaction and measurement updates can patch hosts directly without rendering React; immutable matching plans are reused.
Stable element families
import { createElements } from '@toned/react'
import { buttonStyles } from './styles.ts'
const S = createElements(buttonStyles)
function Button() {
return <S size="m" variant="accent">
<S.Root as="button"><S.Label as="span">Save</S.Label></S.Root>
</S>
}The provider is hostless and carries the current render snapshot to its parts. Independent standalone parts use base/default styles; relationships and grid parts require shared scope. Existing useBind/$scope APIs remain compatible. Use prop bags when spreading onto a host is the better fit.
Usage Patterns
Static Styles (No Variants)
For stylesheets without variants, call useStyles with just the stylesheet:
import { useStyles } from '@toned/react'
import { cardStyles } from './styles.ts'
function Card({ children }: { children: React.ReactNode }) {
const s = useStyles(cardStyles)
return <div {...s.Root}>{children}</div>
}Dynamic Variants
For stylesheets with variants, pass the variant state as the second argument. The styles follow the state whenever it changes:
function NavLink({ href, label, isActive }: {
href: string; label: string; isActive: boolean
}) {
const s = useStyles(navStyles, {
active: isActive,
})
return <a href={href} {...s.Link}>{label}</a>
}Forwarding Props
Since useStyles returns plain props objects, you can combine them with additional props:
function Input({ error, ...rest }:
React.ComponentProps<'input'> & { error: boolean }
) {
const s = useStyles(inputStyles, { error })
return <input {...s.Input.withProps<'input'>(rest)} />
}Two Parts on One Element
.with() merges another part of the same sheet onto the element, and skips a falsy argument. Each part keeps its own styles there, so removing one leaves the other intact. For a state with a fixed set of values, a variant is the simpler declaration.
function Field({ invalid }: { invalid: boolean }) {
const s = useStyles(fieldStyles)
return <input {...s.Input.with(invalid && s.Invalid)} />
}Spread a prop bag whole and last. It carries the ref that attaches the part, so a ref, style or className written before the spread is replaced by it; pass those through withProps, which merges them.
Styles used on this page
The components above import these sheets. They use the base system's tokens; see stylesheet and variants for the declarations themselves.
import type { Variants } from '@toned/core'
import { stylesheet } from '@toned/systems/base'
export const cardStyles = stylesheet({
Root: { bgColor: 'elevated', borderRadius: 'large', padding: 2 },
})
export const buttonStyles = stylesheet({
Root: { $kind: 'pressable', bgColor: 'action', borderRadius: 'medium' },
Label: { $kind: 'text', textColor: 'on_action' },
}).variants(($: Variants<{
size: 'm' | 's'
variant: 'accent' | 'danger'
}>) => ({
[$.variant('danger')]: {
Root: { bgColor: 'destructive' },
Label: { textColor: 'on_destructive' },
},
[$.size('m')]: { Root: { paddingX: 3 } },
[$.size('s')]: { Root: { paddingX: 2, paddingY: 1 } },
}))
export const navStyles = stylesheet({
Link: { $kind: 'text', textColor: 'muted' },
}).variants(($: Variants<{ active: boolean }>) => ({
[$.active(true)]: { Link: { textColor: 'action' } },
}))
export const fieldStyles = stylesheet({
Input: { borderWidth: 'thin', borderColor: 'default', borderRadius: 'medium' },
Invalid: { borderColor: 'status_error' },
})
export const inputStyles = stylesheet({
Input: { borderWidth: 'thin', borderColor: 'default', borderRadius: 'medium' },
}).variants(($: Variants<{ error: boolean }>) => ({
[$.error(true)]: { Input: { borderColor: 'status_error' } },
}))