State System
How Byteion handles component states: default, hover, focus, active, disabled, loading, error.
Overview
Every interactive Byteion component supports a consistent set of states: default, hover, focus, active, disabled, loading, and error. These states apply across buttons, inputs, tables, modals, alerts, toasts, and async workflows. Consistent state handling makes SaaS dashboards, admin panels, ERP/POS screens, ecommerce back offices, internal tools, templates, starter kits, and plugins feel predictable and accessible.
Inconsistent states create confusing interfaces. If a button shows a loading spinner but a table does not show a loading skeleton, users cannot predict how the system communicates progress. This guide defines the Byteion state vocabulary and its implementation rules.
State model
Core interaction states
These states apply to every interactive component. They are primarily visual and rely on CSS pseudo-classes plus HTML attributes.
| State | When to use | Visual cue | Behaviour / accessibility |
|---|---|---|---|
| default | Resting, ready for interaction | Base colours, no modifiers | Focusable if interactive |
| hover | Pointer is over the element | hover:bg-muted or hover:bg-primary/90 |
Does not apply on touch devices reliably |
| focus-visible | Keyboard or programmatic focus | focus-visible:ring-2 focus-visible:ring-primary/40 |
Never remove without a focus-visible replacement |
| active | Being pressed or clicked | active:scale-[0.98] on buttons |
Brief — typically <200ms |
| disabled | Action unavailable or in progress | opacity-60 cursor-not-allowed |
Use native disabled attribute where possible; fall back to aria-disabled when native disabled is not supported |
Supporting states
These states are contextual — they apply to specific component types or workflows.
| State | When to use | Visual cue | Behaviour / accessibility |
|---|---|---|---|
| loading | Async operation in progress | Spinner + disabled state + action text ("Saving…") | Must combine with disabled to prevent double submission; use aria-busy="true" or visible status text |
| error | Validation or operation failure | border-danger + ring-danger/20 + error message |
Use aria-invalid="true" and aria-describedby pointing to error message |
| success | Operation completed successfully | Success icon + green colour on Alerts / Toasts | Often transient; show for 3-5 seconds via Toast |
| warning | Non-blocking issue or confirmation needed | Warning icon + amber colour on Alerts | Requires user acknowledgement or dismissal |
| selected | Row or item currently chosen | bg-primary-50/60 row highlight |
Use aria-selected="true" or aria-current="true" |
| expanded / collapsed | Accordion, collapsible, disclosure widget | Rotate chevron icon, toggle visibility | Use aria-expanded="true/false" on the toggle |
| checked / unchecked | Checkbox, radio, switch | Filled icon or toggle position | Use native checked attribute + aria-checked |
| readonly | Field visible but not editable | No input border change on interaction | Use native readonly attribute |
| empty | No data to display | Centered illustration or message | Communicates "no results" or "no records" — not an error |
| skeleton / loading placeholder | Content not yet loaded | Animated grey blocks matching intended layout | Prevents layout shift; use aria-busy="true" on the container |
| dirty / unsaved | Form has unsubmitted changes | Dot indicator or "Unsaved changes" text | Warn before navigating away |
Button state examples
Buttons use all core states. Loading must combine with disabled to prevent double submission. All examples use semantic tokens — no text-white, no bg-blue-500.
<!-- Default -->
<button type="button"
class="inline-flex items-center justify-center gap-2 rounded-button bg-primary px-4 py-2 text-sm font-medium text-primary-foreground shadow-button transition hover:bg-primary/90 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/40 active:scale-[0.98] disabled:pointer-events-none disabled:opacity-60">
Save changes
</button>
<!-- Hover (applied via CSS pseudo-class) -->
<!-- Focus-visible (applied via CSS pseudo-class) -->
<!-- Active (applied via CSS pseudo-class) -->
<!-- Loading -->
<button type="button" disabled
class="inline-flex items-center justify-center gap-2 rounded-button bg-primary px-4 py-2 text-sm font-medium text-primary-foreground shadow-button opacity-60 cursor-not-allowed">
<svg class="h-4 w-4 animate-spin" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4" />
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" />
</svg>
Saving…
</button>
<!-- Disabled -->
<button type="button" disabled
class="inline-flex items-center justify-center gap-2 rounded-button bg-primary px-4 py-2 text-sm font-medium text-primary-foreground opacity-60 cursor-not-allowed">
Save changes
</button>
<!-- Destructive loading -->
<button type="button" disabled
class="inline-flex items-center justify-center gap-2 rounded-button bg-danger px-4 py-2 text-sm font-medium text-danger-foreground opacity-60 cursor-not-allowed">
<svg class="h-4 w-4 animate-spin" viewBox="0 0 24 24" fill="none" aria-hidden="true">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4" />
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" />
</svg>
Deleting…
</button>
Input error example
Production-ready error state with accessible attributes. The error message is linked via aria-describedby and uses text-danger. The input uses aria-invalid="true" and border-danger.
<div class="space-y-1.5">
<label for="email" class="block text-sm font-medium text-foreground">
Email address
</label>
<input id="email"
type="email"
value="invalid-email"
aria-invalid="true"
aria-describedby="email-error"
class="w-full rounded-input border border-danger bg-background px-3 py-2 text-sm text-foreground outline-none placeholder:text-muted-foreground focus:border-danger focus:ring-2 focus:ring-danger/20">
<p id="email-error" class="flex items-center gap-1 text-sm text-danger" role="alert">
<i class="ti ti-alert-circle text-xs" aria-hidden="true"></i>
Please enter a valid email address.
</p>
</div>
Accessibility rules
- Never remove focus rings without a
focus-visiblereplacement. Removingoutline: nonewithout addingfocus-visible:ring-*makes the page unusable for keyboard users. - Use native
disabledattribute where possible (buttons, inputs, selects). It prevents interaction at the browser level and is announced by screen readers. - Use
aria-disabled="true"only when the element cannot be disabled natively (links, custom widgets). - Use
aria-invalid="true"andaria-describedbyfor form errors so screen readers announce the error. - Use
aria-busy="true"on regions that are updating asynchronously (tables, panels) when a visible loading indicator is present. - Use
role="alert"oraria-live="polite"for async success and error messages that appear without user focus. - Do not communicate state with colour only — always include a text label, icon, or ARIA attribute alongside colour cues.
Async state rules
- Loading buttons must prevent double submission. Combine
disabledwith a spinner and action-specific text ("Saving…", "Uploading…", "Deleting…"). - Loading state should preserve layout — do not shrink or move elements when switching to loading. Use skeleton placeholders for content areas.
- Use action-specific loading text so users know what operation is in progress. Generic "Loading…" is less informative than "Saving changes…".
- Tables should use skeleton rows or an empty/loading state while data is being fetched. Avoid showing a blank white rectangle.
- Errors should provide recovery actions — a "Retry" button, a "Go back" link, or a dismiss action — not just red text.
Practical scenarios
Form submission flow
Default (inputs + submit button) → Focus (user tabs into input, focus-visible:ring) → Input error (aria-invalid + border-danger + error message) → Loading (button spinner + disabled) → Success (Toast with check icon) or Server error (Alert with recovery action).
Table loading and empty state
Default (populated rows) → Loading (skeleton rows matching table layout, aria-busy="true" on table region) → Empty (centered illustration + "No records found" message) → Error (Alert banner above table: "Failed to load data — ").
Modal save flow
Default (form fields + Save button) → Dirty (indicator when form changes) → Loading (Save button shows spinner + "Saving…" + disabled; modal should not close until API responds) → Success (modal closes, Toast confirms save) → Error (inline error in modal body, Save button re-enabled).
Destructive delete confirmation
Default (Cancel + Delete buttons) → Loading (Delete button: bg-danger spinner + "Deleting…" + disabled) → Success (modal closes, Toast confirms deletion, row removed from table) → Error (Toast or Alert: "Failed to delete — ").
Ecommerce product update flow
Default (product form + Save button) → Loading (Save shows "Saving product…", all form inputs disabled) → Validation error (inline on specific fields with border-danger + aria-describedby) → Success (Toast: "Product updated") → Server error (Alert in form: "Could not save — network error").
ERP / POS transaction submit flow
Default (order summary + Submit button) → Loading (Submit: "Processing payment…" + disabled; spinner on payment icon) → Success (receipt modal or Toast: "Payment approved") → Error (Alert: "Transaction declined — " or "Retry with different card").
Do and Don't
Do
- Use consistent state tokens across all components
- Combine loading with disabled for submit actions
- Provide visible, actionable error messages
- Preserve keyboard focus visibility with
focus-visible:ring - Test every state in light and dark mode
- Use skeleton placeholders during content loading
- Make error states recoverable (Retry, Go back)
Don't
- Use disabled state to hide errors — show the error instead
- Remove focus rings without a
focus-visiblereplacement - Show loading without disabling — users will double-click
- Communicate errors with colour only — include text or icons
- Replace semantic HTML attributes (
disabled,readonly,checked) with CSS-only states - Use generic "Loading…" text — be specific: "Saving…", "Deleting…"
Developer tips
Test every component in all states. Before shipping a component, verify default, hover, focus-visible, active, disabled, loading, and error states. Use the browser devtools to force pseudo-classes.
Build state examples first. When creating a new component, start with the state variants before adding custom functionality. This ensures states are not an afterthought.
Prefer native attributes first. Use disabled, readonly, required, checked, and selected before reaching for ARIA. Native attributes are free accessibility wins.
Keep state text human-readable. "Saving changes…" is clearer than "Processing…". "Could not save. Check your connection." is clearer than "Error 500".
Verify dark mode contrast. Error reds (border-danger, text-danger) must maintain WCAG contrast in both light and dark themes. Test with real theming tokens.
Copy this button class string as your starting point for all primary buttons: inline-flex items-center justify-center gap-2 rounded-button bg-primary px-4 py-2 text-sm font-medium text-primary-foreground shadow-button transition hover:bg-primary/90 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/40 active:scale-[0.98] disabled:pointer-events-none disabled:opacity-60.
Related
- Design Principles — the philosophy behind Byteion tokens and components
- Accessibility — ARIA, keyboard navigation, and screen reader support
- Button — button states and variants
- Input — input states, validation, and error patterns
- Form Field — form-level validation and submission states
- Alert — success, warning, error, info banners
- Toast — transient success and error notifications
- Table — loading skeleton, empty state, error state
- Modal — save, loading, and error states in dialogs
- Theming Guide — customising tokens and dark mode contrast
Unlock Premium Patterns
Get access to copy-paste ready Pro blocks, SaaS templates, and advanced component patterns.
Explore Pro Components