Skip to content

Interaction states

Edit on GitHub

Some utility classes can be applied conditionally by adding a modifier prefix to the beginning of the class name, describing the condition you want to target. Only the combinations in Supported utility classes are available in @volvo-cars/css. Modifiers based on viewport size are described in Responsive design.

Commonly used interaction states such as hover and active can get conditional styles depending on their state using the modifier prefixes hover: and active: – for a few select styles. For example, to set the border colour of an element when it is hovered, use the hover:border-primary class.

Foreground colors generally stay the same on hover. hover:text-primary is available for cases such as links in groups with selected states.

focus:border-accent-blue changes the border color when the element matches :focus-visible. It is the only focus: utility. The standard keyboard focus outline still applies; do not override it. Use semantic interactive elements, or tabindex="0" when a custom interactive element needs to enter the keyboard tab order.

Hover utilities require the hover stylesheet, included by @volvo-cars/css/styles_all-media.css. They apply only on devices that support hover.

Design system styles for states such as selected, checked, active have similar modifiers, allowing you to conditionally apply styles to elements depending on their state.

States are mapped to standard HTML attributes and aria-attributes. Using standard attributes instead of toggling class names ensures that the visible behaviour and accessibility attributes stay in sync, giving you a single source of truth for the state of your components.

To style an element based on the state of an ancestor, some common interactive elements have ancestor modifiers. For example, use link-current:border-primary on a decorative child of a current link. checked: and selected: match ARIA attributes; they do not match native :checked or an <option>’s selected attribute.

ModifierApplies
active:To the element while it matches :active
hover:To the hovered element, on devices with hover support
focus:To the element when it matches :focus-visible
button-hover:When an ancestor <button> is hovered
link-hover:When an ancestor <a> is hovered
current:To an element with aria-current="true", "page", or "step"
link-current:When an ancestor <a> has aria-current="true", "page", or "step"
checked:To an element with aria-checked="true"
checkbox-checked:When an ancestor has role="checkbox" and aria-checked="true"
radio-checked:When an ancestor has role="radio" and aria-checked="true"
expanded:To an element with aria-expanded="true"
button-expanded:When an ancestor <button> has aria-expanded="true"
selected:To an element with aria-selected="true"
tab-selected:When an ancestor has role="tab" and aria-selected="true"
open:To the element with an open attribute
details-open:To a direct child of details[open], or a descendant of its direct-child <summary>
odd:To an element that matches :nth-child(odd)
empty:To an element that matches :empty

Combine each prefix below with only the classes in the same row. For example, the checked: row permits checked:border-ring-2. Prefixes are not freely interchangeable or stackable with breakpoint prefixes.

The Colors reference lists each complete class name and its token.

PrefixAvailable color utilities
hover:bg-primary-subtle, text-primary, border-primary
focus:border-accent-blue
current:, checked:, selected:bg-surface-neutral, text-primary, text-inverted, border-primary, border-accent-blue
selected:, expanded:bg-accent-blue-medium
expanded:border-primary, border-accent-blue
link-current:, radio-checked:, checkbox-checked:, tab-selected:, button-expanded:border-primary, border-accent-blue
open:border-primary
odd:bg-secondary

hover:bg-primary-subtle is suppressed while aria-selected="true". bg-primary-subtle and bg-accent-blue-medium have no unprefixed classes.

PrefixAvailable utilities
checked:, selected:border, border-2, border-ring, border-ring-2
link-current:, radio-checked:, checkbox-checked:, tab-selected:border, border-2

Widths and rings use the current border color. See Borders for their layout behavior. Directional widths such as border-b have no state variants.

PrefixAvailable utilities
active:scale-pressed
hover:, button-hover:, link-hover:scale-zoomed
button-expanded:, details-open:rotate-180
empty:hidden, invisible

See Transitions & transforms for motion examples and Display for responsive visibility variants.

These classes apply directly, with no state or responsive prefixes.

ClassEffect
pointer-events-nonepointer-events: none
pointer-events-autopointer-events: auto
cursor-pointerPointer cursor
cursor-autoBrowser-selected cursor
select-nonePrevent text selection (user-select: none)

pointer-events-none controls pointer targeting; it does not disable keyboard interaction. Use the control’s disabled state when disabling an action.

For example, to set the border colour of an element when it is selected, use the selected:border-primary class and toggle the selected state using the aria-selected attribute. Click the examples below to see how the styles change with the attribute value.

Select an option

Show codeHide code
<h2 class="font-medium" id="label">Select an option</h2>
<div class="flex gap-8 mt-16" role="radiogroup" aria-labelledby="label">
<div
role="radio"
class="border px-16 py-8 transition-colors checked:border-accent-blue checked:border-2 hover:border-primary"
aria-checked="true"
tabindex="0"
>
Option 1
</div>
<div
role="radio"
class="border px-16 py-8 transition-colors checked:border-accent-blue checked:border-2 hover:border-primary"
aria-checked="false"
tabindex="-1"
>
Option 2
</div>
</div>
Show codeHide code
<div role="tablist" class="flex gap-16">
<button
role="tab"
class="border-b-2 border-transparent selected:border-accent-blue p-8"
aria-selected="true"
>
Tab 1
</button>
<button
role="tab"
class="border-b-2 border-transparent selected:border-accent-blue p-8"
aria-selected="false"
>
Tab 2
</button>
</div>

When creating your own utility classes or component styles you can use the same approach to apply styles based on the state of the element. If no native HTML or aria attribute is available you can use data- attributes.

.nav-link[aria-current='page'] {
}
.nav-button[disabled] {
}
.nav-button[data-size='large'] {
}
<>
<a href="#" className="nav-link" aria-current={isActive ? 'page' : undefined}>
Link
</a>
<button className="nav-button" disabled={isDisabled} data-size={size}>
Button
</button>
</>
.nav-link.nav-link--is-active {
}
.nav-button.nav-button--is-disabled {
}
.nav-button.nav-button--large {
}
<>
<a href="#" className={`nav-link${isActive ? ' nav-link--is-active' : ''}`}>
Link
</a>
<button
className={[
'nav-button',
isDisabled && 'nav-button--is-disabled',
size && `nav-button--${size}`,
].filter(Boolean)}
>
Button
</button>
</>