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 need role="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 dropdown
  • role="dialog" does not trap focus
  • aria-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-labelAccessible nameIcon-only buttons, unlabeled controls
aria-labelledbyUse visible element as nameModals, dialogs, panels
aria-describedbyLink help / error textInputs, forms, dialogs
aria-expandedOpen / closed stateDropdowns, accordions, disclosure buttons
aria-controlsRelationship to controlled elementTabs, accordions, dropdown triggers
aria-haspopupIndicates popup typeMenu / dropdown triggers where appropriate
aria-selectedSelected stateTabs, option-like controls
aria-currentCurrent itemSidebar / nav active page
aria-invalidInvalid stateForm fields with validation errors
aria-disabledDisabled state when native disabled is not possibleCustom controls only
aria-busyLoading / updating stateAsync regions, tables, panels
aria-hiddenHide decorative contentIcons and purely decorative elements
aria-liveAnnounce dynamic updatesToasts, async status messages
role="dialog"Dialog semanticsModals
role="tablist / tab / tabpanel"Tab structureTabs
role="alert"Assertive announcementCritical errors
role="status"Polite announcementNon-urgent status / toast messages

Component-specific ARIA patterns

Component pattern Required / recommended ARIA
Icon buttonaria-label; icon uses aria-hidden="true"
Dropdown / menu triggeraria-expanded; aria-controls when controlling a specific panel; aria-haspopup where appropriate
AccordionTrigger uses aria-expanded; aria-controls; panel has matching ID
Modal / dialogrole="dialog"; aria-modal="true"; aria-labelledby; optional aria-describedby
Tabsrole="tablist"; tabs use role="tab", aria-selected, aria-controls; panels use role="tabpanel"
Form fieldLabel connection; aria-invalid; aria-describedby for help / error
Toast / alertrole="status" for polite updates; role="alert" for urgent errors
NavigationCurrent page uses aria-current="page"
Loading panelaria-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 need role="button"
  • Pair aria-expanded with aria-controls when a trigger controls a specific element with an ID
  • Keep ARIA state synchronised with visual state
  • Use visible labels when possible
  • Use aria-label only when visible text is missing or insufficient
  • Prefer disabled over aria-disabled for 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-label that 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, or aria-busy when 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, and aria-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.

Unlock Premium Patterns

Get access to copy-paste ready Pro blocks, SaaS templates, and advanced component patterns.

Explore Pro Components