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.
Roadie provides CSS utilities that encode many of the guidelines on this page automatically. Apply them to elements that respond to user input.
is-interactiveFor 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.
| Behaviour | How |
|---|---|
| Cursor | cursor: pointer |
| Transitions | Colour, shadow, outline, and transform, from the motion tokens |
| Active press | scale(0.99) |
| Focus ring | :focus-visible outline, intent-coloured with transparency |
| Disabled | opacity: 0.5, pointer-events: none, and grayscale(0.3) |
--interactive-transition sets each property that animates, with its
duration and its easing token.
background-color200msease-standardborder-color200msease-standardcolor200msease-standardbox-shadow200msease-standardoutline-width150msease-standardoutline-color150msease-standardtransform150msease-springRoadie 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.
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.
--focus-ring-width--focus-ring-opacity--focus-ring-opacity-darkis-interactive-withinFor 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.
is-focusableFor 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.
is-interactive-fieldFor 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.
| State | Background | Border | Outline |
|---|---|---|---|
| Rest | From the emphasis | From the emphasis | None |
| Hover | neutral-3 (neutral-2 in dark) | neutral-7 | None |
| Focus | accent-2 (accent-1 in dark) | accent-9 | Accent ring |
| Invalid | danger-2 | danger-9 | Danger ring |
A disabled field takes opacity: 0.5, pointer-events: none, and
grayscale(0.3).
is-interactive-field-groupFor 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.
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.
| Utility | State | Label and fill |
|---|---|---|
is-unselected | Rest | Subtle text, a step up to normal text on hover |
emphasis-subtle is-selected | Chosen | Strong text on a soft fill, with a Highlight edge under forced colours |
is-selected-label | Chosen, over an indicator | Strong text, while the indicator behind draws the fill |
is-selected-label-on-strong | Chosen, over a strong indicator | The 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 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 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.
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.
The classes the component library applies.
| Component | Classes |
|---|---|
| Button (strong) | btn is-interactive emphasis-strong |
| Button (normal) | btn is-interactive emphasis-normal |
| Button (subtler) | btn is-interactive emphasis-subtler |
| Text input | emphasis-field is-interactive-field |
| Select | emphasis-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 |
These guide how we handle interaction across Oztix applications. Follow them when building custom interactive UI.
is-interactive,
is-interactive-field, is-interactive-field-group,
is-interactive-within, or is-focusable, so every control moves and
focuses the same way.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.
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.
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.
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.
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.
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.
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".
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 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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
| Element | Utility | Emphasis | Notes |
|---|---|---|---|
| Button | is-interactive | strong, normal, subtle, or subtler | Add btn for the full radius |
| Text input | is-interactive-field | field | None |
| Select | is-interactive-field | normal | None |
| Textarea | is-interactive-field | field | None |
| Clickable card | is-interactive | raised | Add rounded-xl |
| Toggle or radio | is-interactive | subtler or normal | None |
| Scroll region | is-focusable | Any | Add tabIndex={0}, a region role, and a name |
| Link | is-focusable, through link-* | None | Use Link, or link-normal, link-strong, or link-subtle, which bring the focus ring and hover motion |