RoadieRoadie
  • Home
  • Brand
  • Foundations
  • Content
  • Tokens
  • Components
  • Charts
  • Widgets
  • Appearance
HomeBrandFoundations
Appearance

Foundations

  • Visual

    • Colors
    • Elevation
    • Iconography
    • Shape
    • Theming
  • Content

    • Date and time
    • Layout
    • Prose styles
    • Typography
  • Behaviour

    • Accessibility
    • Interactions
    • Motion
    • View transitions
  • Building apps

    • Forms
    • Linking
    • Navigation
    • Performance
    • Records modelAlpha
    • Tables

Interactions

Docs updated
23 Oct 2026

ReferenceEmphasis and states tokens

Interaction is a conversation. Details are the vocabulary. We build for the felt experience. We care about what feels right, not just what works.

Interaction utilities

Roadie provides CSS utilities that encode many of the guidelines on this page automatically. Apply them to elements that respond to user input.

is-interactive

For clickable elements, such as buttons, cards, links, and toggles. Provides cursor, transitions, active press, focus ring, and disabled state. Pair with an emphasis-* class for visual styling. Hover, press, and tab through these buttons to see transitions, active press, and focus rings.

{/* Strong */}
<Button emphasis='strong'>Buy tickets</Button>
{/* Normal */}
<Button emphasis='normal'>Buy tickets</Button>
{/* Subtle */}
<Button emphasis='subtle'>Buy tickets</Button>
BehaviourHow
Cursorcursor: pointer
TransitionsColour, shadow, outline, and transform, from the motion tokens
Active pressscale(0.99)
Focus ring:focus-visible outline, intent-coloured with transparency
Disabledopacity: 0.5, pointer-events: none, and grayscale(0.3)

--interactive-transition sets each property that animates, with its duration and its easing token.

  1. background-color200msease-standard
  2. border-color200msease-standard
  3. color200msease-standard
  4. box-shadow200msease-standard
  5. outline-width150msease-standard
  6. outline-color150msease-standard
  7. transform150msease-spring

Roadie has one focus ring shape. It's 4px wide and sits flush against the element with no gap. The base styles draw it on a focused element without a Roadie class, such as a link in CMS content or a plain button, and is-interactive and is-focusable draw the same ring and animate it in. Its colour follows the nearest intent-* ancestor. Tab through these to see the ring change.

{/* Neutral */}
<Button emphasis='strong' intent='neutral'>Neutral</Button>
{/* Accent */}
<Button emphasis='strong' intent='accent'>Accent</Button>
{/* Danger */}
<Button emphasis='strong' intent='danger'>Danger</Button>

Fields use the same ring in accent, or danger when invalid.

The ring's width, and its opacity in light and dark mode, come from the focus ring tokens.

Width--focus-ring-width
4px
Opacity in light mode--focus-ring-opacity
30%
Opacity in dark mode--focus-ring-opacity-dark
20%

is-interactive-within

For a surface whose main link sits inside it, such as a card that also holds other links or buttons. Mark that link with data-interactive-target. Its overlay covers the surface, so a click anywhere follows it. Other controls sit above the overlay and stay clickable. The surface takes the same hover, press, and focus states as is-interactive while its main link is hovered, pressed, or focused. It does nothing until a target is present. Card applies it for you when it holds a Card.Link.

Surfaces can nest. A target belongs to the nearest surface around it, so a card around a grouped DataTable or RecordTable list rows stays still while one of those rows is hovered, pressed, or focused.

<Card emphasis='raised'>
<Card.Content>
<Card.Title>
<Card.Link href='#is-interactive-within'>Scans by gate</Card.Link>
</Card.Title>
<DataTable
<Card emphasis='raised'>
<Card.Content>
<Card.Title>
<Card.Link href='#is-interactive-within'>Midnight Frequency</Card.Link>
</Card.Title>
<Card.Description>Clockwork Wattle Brewery, Brisbane</Card.Description>

is-focusable

For an element that takes focus but isn't clicked, such as a scroll region or a tabIndex={-1} landing target. It gives the same focus ring as is-interactive, from the same tokens, with no pointer cursor, press, or disabled state. The docs' wide tables use it on their scroll region. Tab into tonight's program to see the ring.

<div
className='is-focusable emphasis-sunken grid max-h-32 gap-2 overflow-y-auto rounded-xl p-4'
tabIndex={0}
role='region'
aria-label="Tonight's program"
>

is-interactive-field

For form inputs, such as text fields, textareas, and selects. Provides state-based colour transitions: neutral at rest, accent on focus, and danger when invalid. Pair with emphasis-field for text fields, or emphasis-normal for select triggers. Focus each input to see the accent border and ring. The invalid field shows danger styling.

{/* Default */}
<Field>
<Field.Label>Name</Field.Label>
<Field.Input placeholder='e.g. Sam Okafor' />
</Field>
{/* Invalid */}
StateBackgroundBorderOutline
RestFrom the emphasisFrom the emphasisNone
Hoverneutral-3 (neutral-2 in dark)neutral-7None
Focusaccent-2 (accent-1 in dark)accent-9Accent ring
Invaliddanger-2danger-9Danger ring

A disabled field takes opacity: 0.5, pointer-events: none, and grayscale(0.3).

is-interactive-field-group

For composite form controls where multiple inputs share a single visual container, like the Combobox input group (a text input and a trigger button). Provides the same state transitions as is-interactive-field, but scoped to the group wrapper instead of an individual input.

Use it on the wrapping element that contains the child inputs. Focus within any child triggers the accent transition on the group. Pair with emphasis-field, or emphasis-normal for a button-like trigger.

Selection

is-unselected and is-selected are for a set of choices, such as a toggle, a toggle group, or tabs. These utilities set the label colour in every state, with whichever intent applies, so a component never sets it itself. Apply each one only in its own state, as in not-data-[pressed]:is-unselected data-[pressed]:emphasis-subtle data-[pressed]:is-selected, so the resting and chosen utilities never meet. A subtler ToggleGroup uses them in every intent.

{/* Neutral */}
<ToggleGroup emphasis='subtler' intent='neutral' aria-label='Range' defaultValue={['week']}>
<ToggleGroup.Item value='day'>Day</ToggleGroup.Item>
<ToggleGroup.Item value='week'>Week</ToggleGroup.Item>
<ToggleGroup.Item value='month'>Month</ToggleGroup.Item>
</ToggleGroup>
UtilityStateLabel and fill
is-unselectedRestSubtle text, a step up to normal text on hover
emphasis-subtle is-selectedChosenStrong text on a soft fill, with a Highlight edge under forced colours
is-selected-labelChosen, over an indicatorStrong text, while the indicator behind draws the fill
is-selected-label-on-strongChosen, over a strong indicatorThe label colour of emphasis-strong

A browser test holds the pair in every intent, in light and dark, on the page and on a card:

  • The chosen label reaches APCA Lc 75 on its fill, the minimum for body text.
  • It beats a resting label on the same surface by at least Lc 15, so the label alone tells them apart.
  • A resting label reaches Lc 45, the minimum for non-text UI such as an icon.
  • The soft fill sits at least 0.075 OKLCH lightness from the surface. APCA reads two soft fills as Lc 0 in dark mode, so lightness measures them instead.

The soft fill alone doesn't mark the chosen item clearly enough, so it's for quiet controls whose icon or label carries the state too. Use emphasis-strong when the state must stand out.

Emphasis and interaction

Emphasis presets and interaction utilities compose together. The emphasis sets appearance and the hover and active colour shifts. The interaction utility adds mechanical behaviours. Together they form a complete interactive element. Hover each card to compare a static one with an interactive one. A Card with an href takes is-interactive.

{/* Static */}
<Card emphasis='normal'>
<Card.Content>
<Card.Title>Ochre Kite Weekender</Card.Title>
</Card.Content>
</Card>

is-interactive-field provides its own hover, focus, and invalid logic. It doesn't use emphasis hover states. This is why form inputs use emphasis-field, which has no interactive states, paired with is-interactive-field, which provides all of them.

Component recipes

The classes the component library applies.

ComponentClasses
Button (strong)btn is-interactive emphasis-strong
Button (normal)btn is-interactive emphasis-normal
Button (subtler)btn is-interactive emphasis-subtler
Text inputemphasis-field is-interactive-field
Selectemphasis-normal is-interactive-field
Select (subtler)emphasis-subtler is-interactive
Card (raised, href)rounded-xl emphasis-raised is-interactive
Link (normal)link-normal, which includes is-focusable
Radio (subtler)emphasis-subtler is-interactive

Guidelines

These guide how we handle interaction across Oztix applications. Follow them when building custom interactive UI.

  • Never hand-roll hover or focus states. Use is-interactive, is-interactive-field, is-interactive-field-group, is-interactive-within, or is-focusable, so every control moves and focuses the same way.

Keyboard and focus

Keyboard works everywhere

The mouse is optional. Power users and assistive technologies rely on the keyboard.

Do

Ensure all flows are keyboard-operable and follow WAI-ARIA Authoring Practices.

Don’t

Build "clickable" divs that cannot be focused or activated with Enter or Space.

Clear focus indicators

Navigation requires visibility. Users must always know where they are on the page.

Do

Show a visible focus ring for every focusable element. Use :focus-visible to avoid distracting mouse users. Roadie's base styles draw the ring on plain links and buttons, and is-interactive and is-focusable animate it in.

Don’t

Remove outlines (outline: none) without replacing them with a high-contrast alternative.

Manage focus

Context changes require focus changes. Don't leave the user's focus behind when the UI changes.

Do

Use focus traps for modals. Move focus to new content after navigation. Return focus to the trigger when a menu closes.

Don’t

Open a modal but leave the focus on the button that opened it, hidden under the backdrop.

Hit targets and touch

Match visual and hit targets

Fitts' Law applies to code. What looks clickable must be clickable. It must also be a comfortable size.

Do

Expand hit targets to at least 24px on desktop or 44px on mobile, even if the icon is smaller. Use padding to expand the area.

Don’t

Wrap a tiny 12px icon in a click handler with no padding.

Mobile input hygiene

Prevent disorienting zooms. Inputs must be legible by default on touch devices.

Do

Set <input> font size to at least 16px on mobile.

Don’t

Allow iOS Safari to auto-zoom or pan when an input is focused because the text is too small.

No dead zones

Proximity implies relationship. There should be no gap between a label and its control.

Do

Wrap the input and label in a single clickable container. Clicking a visible label should focus its control.

Don’t

Leave an unclickable gap between a checkbox and its text.

Respect user agency

The browser belongs to the user. Never disable native capabilities like zooming or pasting.

Do

Allow pasting in all <input> and <textarea> elements.

Don’t

Disable browser zoom (user-scalable=no) or block paste events for "security".

Forms

Labels are mandatory

Every control needs a name, even if visually hidden.

Do

Ensure every control has a <label> or aria-label. Clicking a visible label should focus its control.

Don’t

Rely on placeholder text as the only label.

Placeholders are examples

Placeholders should show how to answer, not what to answer.

Do

Use "e.g. 0400 000 000" as a placeholder to show the expected format.

Don’t

Repeat the label "Phone number" in the placeholder.

Enter submits

The Enter key is the universal "Done" signal.

Do

Submit the form on Enter when a text input is focused. If multiple controls exist, apply to the last one.

Don’t

Force users to pick up the mouse to click "Save" after typing.

Keep submit active

Validation should be educational, not preventative. Disabled buttons hide the "why."

Do

Keep the submit button active. Show validation errors on click.

Don’t

Disable the submit button while the form is incomplete.

Don't block typing

Inputs accept input. Let the user type, then correct them.

Do

Allow any characters in a field, then show a validation error if they are invalid. Trim leading and trailing whitespace before validation.

Don’t

Silently block keystrokes, such as preventing non-numbers in a phone field, without feedback.

Inline errors

Contextualise failure. Show the error where the problem is.

Do

Display error messages immediately next to the invalid field. Focus the first error on submit.

Don’t

Dump a generic "Form invalid" banner at the top of the page.

Autocomplete everywhere

Browsers are smart; let them help. Correct metadata enables one-click filling.

Do

Set autocomplete attributes and meaningful name values on inputs.

Don’t

Use generic names like input_1 that confuse password managers.

Unsaved changes protection

Data loss is a critical failure. Warn the user before destroying their work.

Do

Trigger a confirmation dialog if the user tries to navigate away with dirty state.

Don’t

Let a stray back-swipe wipe out a 500-word essay.

Feedback and state

Honest loading states

For fast, reversible operations, skip the loader entirely and use optimistic UI instead.

Do

For slow or high-stakes operations, show a loader. Commit to it for at least 300ms. Delay its appearance by 200ms to avoid flash on fast responses.

Don’t

Show a spinner for 50ms then immediately remove it. Flicker creates anxiety.

Optimistic UI

The interface should move as fast as the user's thought. Reserve loading states for high-stakes or slow operations.

Do

For fast, reversible actions, such as toggling a like or reordering a list, update the UI immediately and reconcile with the server in the background.

Don’t

Block the UI with a spinner for every server round-trip, even trivial ones.

Ellipsis implies continuation

An ellipsis indicates that an action is not yet complete or requires more input.

Do

Use "Rename…" (opens a dialog) and "Saving…" (process in flight).

Don’t

Use "Rename" for a button that opens a modal. The user expects immediate action.

Destructive friction

Regret is expensive. Make irreversible actions hard to do by accident.

Do

Require explicit confirmation or provide a generous "Undo" window for destructive actions.

Don’t

Delete a project immediately upon clicking a red trash icon.

Forgiving interactions

Users are imprecise. The interface should anticipate intent, not punish inaccuracy.

Do

Use prediction cones for menus. Add debounce to tooltips, with a delay first and instant subsequent ones.

Don’t

Close a dropdown the millisecond the mouse leaves the trigger pixel.

Quick reference

ElementUtilityEmphasisNotes
Buttonis-interactivestrong, normal, subtle, or subtlerAdd btn for the full radius
Text inputis-interactive-fieldfieldNone
Selectis-interactive-fieldnormalNone
Textareais-interactive-fieldfieldNone
Clickable cardis-interactiveraisedAdd rounded-xl
Toggle or radiois-interactivesubtler or normalNone
Scroll regionis-focusableAnyAdd tabIndex={0}, a region role, and a name
Linkis-focusable, through link-*NoneUse Link, or link-normal, link-strong, or link-subtle, which bring the focus ring and hover motion
Previous page← AccessibilityNext pageMotion →