ARIA Patterns
How to use ARIA attributes correctly across Byteion components.
Overview
ARIA enhances HTML semantics for assistive technologies — it does not add browser behaviour by itself. Byteion applies ARIA where native HTML is not enough: native controls keep their built-in semantics, while composite patterns such as modals, dropdowns, tabs, accordions, and toasts use ARIA to expose state and relationships.
First rule of ARIA
- Use native HTML first — a
<button>does not needrole="button" - Do not use ARIA to turn non-interactive elements into controls unless absolutely necessary
- If it behaves like a button, use
<button> - If it navigates, use
<a href=""> - A visible text button usually does not need
aria-label
ARIA does not add behaviour
ARIA attributes change what assistive technology announces, but they do not make elements interactive:
aria-expanded="true"does not open a dropdownrole="dialog"does not trap focusaria-hidden="true"does not remove an element from keyboard focus if it remains focusable- JavaScript and correct DOM state must keep ARIA attributes synchronised with the UI
Core attribute reference
| Attribute / role | Purpose | Common Byteion usage |
|---|---|---|
| aria-label | Accessible name | Icon-only buttons, unlabeled controls |
| aria-labelledby | Use visible element as name | Modals, dialogs, panels |
| aria-describedby | Link help / error text | Inputs, forms, dialogs |
| aria-expanded | Open / closed state | Dropdowns, accordions, disclosure buttons |
| aria-controls | Relationship to controlled element | Tabs, accordions, dropdown triggers |
| aria-haspopup | Indicates popup type | Menu / dropdown triggers where appropriate |
| aria-selected | Selected state | Tabs, option-like controls |
| aria-current | Current item | Sidebar / nav active page |
| aria-invalid | Invalid state | Form fields with validation errors |
| aria-disabled | Disabled state when native disabled is not possible | Custom controls only |
| aria-busy | Loading / updating state | Async regions, tables, panels |
| aria-hidden | Hide decorative content | Icons and purely decorative elements |
| aria-live | Announce dynamic updates | Toasts, async status messages |
| role="dialog" | Dialog semantics | Modals |
| role="tablist / tab / tabpanel" | Tab structure | Tabs |
| role="alert" | Assertive announcement | Critical errors |
| role="status" | Polite announcement | Non-urgent status / toast messages |
Component-specific ARIA patterns
| Component pattern | Required / recommended ARIA |
|---|---|
| Icon button | aria-label; icon uses aria-hidden="true" |
| Dropdown / menu trigger | aria-expanded; aria-controls when controlling a specific panel; aria-haspopup where appropriate |
| Accordion | Trigger uses aria-expanded; aria-controls; panel has matching ID |
| Modal / dialog | role="dialog"; aria-modal="true"; aria-labelledby; optional aria-describedby |
| Tabs | role="tablist"; tabs use role="tab", aria-selected, aria-controls; panels use role="tabpanel" |
| Form field | Label connection; aria-invalid; aria-describedby for help / error |
| Toast / alert | role="status" for polite updates; role="alert" for urgent errors |
| Navigation | Current page uses aria-current="page" |
| Loading panel | aria-busy="true" while content updates |
Practical examples
<!-- Icon-only button -->
<button type="button" aria-label="Delete item"
class="inline-flex rounded-button bg-danger/10 px-2 py-2 text-danger
focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-danger/20">
<i class="ti ti-trash" aria-hidden="true"></i>
</button>
<!-- Form field with error -->
<label for="email" class="text-sm font-medium text-foreground">Email</label>
<input type="email" id="email" aria-invalid="true"
aria-describedby="email-error"
class="w-full rounded-lg border border-danger bg-background px-3 py-2 text-sm
focus:outline-none focus:ring-2 focus:ring-danger/20">
<p id="email-error" role="alert" class="mt-1 text-sm text-danger">
Please enter a valid email address.</p>
<!-- Modal -->
<div data-bi-modal="confirm" role="dialog" aria-modal="true"
aria-labelledby="modal-title" aria-describedby="modal-desc"
class="rounded-xl bg-card p-6 shadow-panel border border-border">
<h2 id="modal-title" class="text-lg font-semibold text-foreground">Confirm deletion</h2>
<p id="modal-desc" class="text-sm text-muted-foreground">
This action cannot be undone.</p>
<div class="mt-4 flex gap-3">
<button data-bi-modal-hide class="rounded-lg border border-border bg-background px-4 py-2 text-sm">
Cancel</button>
<button class="rounded-lg bg-danger px-4 py-2 text-sm font-medium text-danger-foreground
focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-danger/20">
Delete</button>
</div>
</div>
<!-- Tabs -->
<div role="tablist" aria-label="Settings">
<button role="tab" aria-selected="true" aria-controls="panel-general"
class="rounded-button px-4 py-2 text-sm font-medium text-foreground
focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/20">
General</button>
<button role="tab" aria-selected="false" aria-controls="panel-account"
class="rounded-button px-4 py-2 text-sm font-medium text-muted-foreground
focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/20">
Account</button>
</div>
<div role="tabpanel" id="panel-general">
<p class="text-sm text-muted-foreground">General settings content.</p>
</div>
<div role="tabpanel" id="panel-account" hidden>
<p class="text-sm text-muted-foreground">Account settings content.</p>
</div>
Use role="alert" only when the error appears dynamically after user interaction. Static helper text does not need role="alert".
Best practices
- Prefer native HTML semantics over ARIA — a
<button>does not needrole="button" - Pair
aria-expandedwitharia-controlswhen a trigger controls a specific element with an ID - Keep ARIA state synchronised with visual state
- Use visible labels when possible
- Use
aria-labelonly when visible text is missing or insufficient - Prefer
disabledoveraria-disabledfor native controls - Use
aria-hidden="true"only for decorative, non-focusable content
Common mistakes
Don't
- Add
role="button"to a real<button> - Use
aria-labelthat contradicts visible text - Hide focusable content with
aria-hidden="true" - Use ARIA to fake disabled state on native controls instead of
disabled - Mark an entire interactive panel as
aria-hidden - Forget to update
aria-expanded,aria-selected, oraria-busywhen state changes - Use
role="alert"for every helper message
Do
- Use native HTML first
- Use ARIA for state and relationships only when needed
- Keep IDs stable for
aria-controls,aria-labelledby, andaria-describedby - Test with a screen reader
- Verify keyboard behaviour separately from ARIA
Testing ARIA
- Inspect accessible names in browser DevTools
- Test icon-only buttons with a screen reader
- Check that dropdowns announce expanded / collapsed state
- Check that tabs announce selected state
- Check that form errors are announced when they appear
- Check that modals announce their title and description
- Run Lighthouse or axe, but manually verify screen-reader behaviour
- Automated tools cannot fully validate whether ARIA usage makes sense
Developer tips
• First rule of ARIA: don't use ARIA. Native HTML beats ARIA every time.
• ARIA attributes must match visible UI state. If a dropdown looks closed but
aria-expanded="true", the screen reader gets wrong information.
• Screen-reader testing is required because ARIA is invisible visually — what you declare and what gets announced may differ.
• Byteion components include ARIA where needed, but custom compositions still require review.
• If you add custom behaviour, update keyboard behaviour, focus behaviour, and ARIA state together — they are a single unit of work.Related
- Accessibility Overview — Overall accessibility contract
- Keyboard Navigation — Keyboard behaviour
- Focus System — Focus trap and restoration
- Modal / Dialog — Dialog semantics
- Dropdown / Menu — Dropdown and menu ARIA
- Toast / Alert — Live region behaviour
Unlock Premium Patterns
Get access to copy-paste ready Pro blocks, SaaS templates, and advanced component patterns.
Explore Pro Components