Interactive Styles
toned-styles supports hover, focus, and active states using colon-prefixed keys inside element definitions. On the web with pseudoMode: 'css', these work entirely through CSS with no JavaScript event listeners.
Element-Level Pseudo-Classes
Add :hover, :focus, or :active keys inside an element definition:
const buttonStyles = stylesheet({
container: {
bgColor: 'action',
borderRadius: 'medium',
cursor: 'pointer',
':hover': {
bgColor: 'action_secondary',
},
':active': {
bgColor: 'muted',
},
},
label: {
textColor: 'on_action',
},
})Cross-Element Selectors
To change one element's styles when a different element is interacted with, use the 'element:pseudo' key at the stylesheet root level:
const cardStyles = stylesheet({
container: {
bgColor: 'elevated',
borderRadius: 'large',
cursor: 'pointer',
},
label: {
textColor: 'default',
},
icon: {
textColor: 'muted',
},
// When 'container' is hovered, change styles on multiple elements
'container:hover': {
container: { shadow: 'medium' },
label: { textColor: 'action' },
icon: { textColor: 'action' },
},
})You can combine multiple pseudo-states in a cross-element selector:
'container:active:hover': {
icon: { textColor: 'on_action' },
}Combining with Variants
Pseudo-classes work inside variant blocks, so different variants can define different interactive behaviour:
const buttonStyles = stylesheet({
container: { bgColor: 'action', borderRadius: 'medium' },
label: { textColor: 'on_action' },
}).variants<{
variant: 'accent' | 'danger'
}>(($) => ({
[$.variant('accent')]: {
container: {
bgColor: 'action',
':hover': { bgColor: 'action_secondary' },
},
},
[$.variant('danger')]: {
container: {
bgColor: 'status_error',
':hover': { bgColor: 'status_error', shadow: 'medium' },
},
},
}))
Combining with Breakpoints
Pseudo-classes and breakpoints compose naturally:
const navStyles = stylesheet({
link: {
textColor: 'muted',
paddingX: 2,
':hover': {
textColor: 'action',
},
'@md': {
paddingX: 4,
},
},
})React Native
React Native does not have CSS pseudo-classes. On native platforms, interactive states are handled through React Native's Pressable component. The same :hover and :active keys work in both environments, but the runtime behaviour adapts to each platform's capabilities.
Advanced: How It Works
On the web, interactive styles use the CSS "space toggle" technique. The system declares a custom property for each pseudo-state:
html {
--toned_hover: initial; /* "off" */
--toned_focus: initial;
--toned_active: initial;
}When an element is hovered, a cascade rule flips the variable from initial (off) to an empty value (on):
/* Activate on the hovered element */
._:hover { --toned_hover: ; }
/* Reset for children so hover doesn't leak down */
._:hover ._ { --toned_hover: initial; }
/* Re-activate for nested elements that are themselves hovered */
._:hover ._:hover { --toned_hover: ; }Token values then reference this variable in a var() fallback chain. When the variable is initial, the fallback (base value) is used. When it is empty, the hover value takes effect. This is the same mechanism that powers responsive breakpoints with --media-md, just triggered by CSS pseudo-classes instead of @media queries.