Interactions
Interaction is a conversation. Details are the vocabulary. We build for the felt experience — not just what works, but what feels right.
Design principles
These principles guide how we handle interaction across Oztix applications. Follow them when building custom interactive UI.
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/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 is-interactive provides this automatically.
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 — at a comfortable size.
Do
Expand hit targets to ≥ 24px (desktop) or ≥ 44px (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 ≥ 16px on mobile.
Don’t
Allow iOS Safari to auto-zoom/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. +1 (555) 000-0000” 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/trailing whitespace before validation.
Don’t
Silently block keystrokes (e.g., 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 — but 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 (toggling a like, 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 (delay first, instant subsequent).
Don’t
Close a dropdown the millisecond the mouse leaves the trigger pixel.
Interaction utilities
Roadie provides two CSS utilities that encode many of these principles automatically. Apply them to elements that respond to user input.
is-interactive
For clickable elements — buttons, cards, links, toggles. Provides cursor, transitions, active press, focus ring, and disabled state. Pair with an emphasis-* class for visual styling.
Hover, click, and tab through these buttons to see transitions, active press, and focus rings.
| Behaviour | How |
|---|---|
| Cursor | cursor: pointer |
| Transitions | background, border, color, box-shadow, outline, transform — 0.2s ease |
| Active press | scale(0.99) |
| Focus ring | :focus-visible outline, intent-coloured with transparency |
| Disabled | opacity: 0.5, pointer-events: none, grayscale(0.3) |
Intent-coloured focus rings
The focus ring colour follows the nearest intent-* ancestor. Tab through these to see the ring change.
is-interactive-field
For form inputs — text fields, textareas, selects. Provides state-based colour transitions: neutral at rest, accent on focus, danger when invalid. Pair with emphasis-sunken or emphasis-raised.
Focus each input to see the accent border and ring. The invalid field shows danger styling.
| State | Background | Border | Outline |
|---|---|---|---|
| Rest | from emphasis | from emphasis | none |
| Hover | neutral-2 | neutral-7 | — |
| Focus | accent-2 | accent-9 | accent ring |
| Invalid | danger-2 | danger-9 | danger ring |
| Disabled | opacity: 0.5, pointer-events: none, grayscale(0.3) | ||
is-interactive-field-group
For composite form controls where multiple inputs share a single visual container — like the Combobox input group (text input + trigger button). Provides the same state transitions as is-interactive-field but scoped to the group wrapper instead of an individual input.
Use on the wrapping element that contains the child inputs. Focus within any child triggers the accent transition on the group. Pair with emphasis-raised or emphasis-sunken.
Emphasis + interaction layering
Emphasis presets and interaction utilities compose together. The emphasis sets appearance and hover/active colour shifts. The interaction utility adds mechanical behaviours. Together they form a complete interactive element.
Static vs interactive — hover to see the difference
emphasis-strong (static)
emphasis-strong + is-interactive
Hover, click, or tab to strong
emphasis-normal (static)
emphasis-normal + is-interactive
Hover, click, or tab to normal
emphasis-raised (static)
emphasis-raised + is-interactive
Hover, click, or tab to raised
Note: is-interactive-field provides its own hover, focus, and invalid logic — it does not use emphasis hover states. This is why form inputs use emphasis-sunken (which has no interactive states) paired with is-interactive-field (which provides all of them).
Component recipes
Common patterns from the component library.
| Component | Classes |
|---|---|
| Button (primary) | emphasis-strong is-interactive rounded-full |
| Button (secondary) | emphasis-normal is-interactive rounded-full |
| Button (subtler) | emphasis-subtler is-interactive rounded-full |
| Text input | emphasis-sunken border border-subtle is-interactive-field |
| Select | emphasis-raised border border-normal is-interactive-field |
| Clickable card | emphasis-raised is-interactive rounded-xl |
| Radio (inline) | emphasis-subtler is-interactive |
Quick reference
| Element | Utility | Emphasis | Notes |
|---|---|---|---|
| Button | is-interactive | strong / default / subtle / subtler | Add rounded-full |
| Text input | is-interactive-field | sunken | Add border border-subtle |
| Select | is-interactive-field | raised | Add border border-normal |
| Textarea | is-interactive-field | sunken | Add border border-subtle |
| Clickable card | is-interactive | raised | Add rounded-xl |
| Toggle / radio | is-interactive | subtler or default | — |
| Link | none (native) | — | Use underline underline-offset-2 |