toned-styles
Overview
Getting StartedCore Concepts
API Reference
defineSystemstylesheetvariantsuseStylesMedia Queries
Guides
React WebReact NativeThemingInteractive StylesSSR & SSG

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.