Screen Reader Support

How Byteion components announce to screen reader users.

Overview

Screen readers convert the accessibility tree into speech or braille. Byteion components should expose meaningful names, roles, states, and relationships. Visual-only UI is not enough — state changes must be announced when needed.

Why this exists

Many people rely on screen readers, braille displays, magnification, or other assistive technologies to use the web. A modal that opens silently, an unlabeled icon button, or an unannounced form error can make a product workflow impossible to complete. Byteion components aim to announce enough information for a screen-reader user to understand and operate the interface.

Core rule: native HTML first

  • Use <button> for actions, <a> for navigation, <label> for form fields, <fieldset> and <legend> for grouped controls
  • Add ARIA only when native semantics do not fully describe the UI
  • Do not use ARIA to fake behaviour that the component does not actually support

Core principles

  • Semantic structure — Headings, landmarks, lists, buttons, links, and form labels create a navigable page
  • Accessible names — Every control needs a clear name from visible text, <label>, aria-label, or aria-labelledby
  • State announcements — Expanded, selected, checked, disabled, invalid, and busy states should be exposed
  • Relationships — Connect triggers with panels via aria-controls, dialogs with aria-labelledby, fields with aria-describedby
  • Live regions — Announce dynamic updates only when users need to hear them
  • Focus management — Move focus into newly opened UI and restore focus when it closes

Byteion component patterns

Buttons and icon buttons

Text buttons usually get their accessible name from visible text. Icon-only buttons need aria-label or sr-only text:

<!-- aria-label approach -->
<button type="button" aria-label="Open user menu"
    class="rounded-button bg-background px-2 py-2 text-foreground
           focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/20">
 <i class="ti ti-dots-vertical" aria-hidden="true"></i>
</button>

<!-- sr-only approach -->
<button type="button"
    class="rounded-button bg-background px-2 py-2 text-foreground
           focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/20">
 <span class="sr-only">Open user menu</span>
 <i class="ti ti-dots-vertical" aria-hidden="true"></i>
</button>

Forms and validation

Use <label> for every input. Use aria-invalid="true" only when the field has an error. Use aria-describedby to connect help text or error messages:

<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" class="mt-1 text-sm text-danger">
 Enter a valid email address.</p>

Modal dialogs

Use role="dialog" or native <dialog> where supported. Add aria-modal="true" and aria-labelledby pointing to the modal title. Move focus into the modal when opened, trap focus while open, and return focus to the trigger when closed:

<div data-bi-modal="settings" role="dialog" aria-modal="true"
     aria-labelledby="settings-title"
     class="rounded-xl bg-card p-6 shadow-panel border border-border">
 <h2 id="settings-title" class="text-lg font-semibold text-foreground">Settings</h2>
 <p class="text-sm text-muted-foreground">Configure your preferences.</p>
</div>

Dropdowns

The trigger should expose expanded state. Use aria-expanded and aria-controls when the menu panel has an ID. Menu items must be keyboard reachable:

<button type="button" aria-expanded="false" aria-controls="user-menu"
    class="rounded-button bg-background px-4 py-2 text-sm text-foreground
           focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/20">
 User menu
</button>
<div id="user-menu" hidden
     class="rounded-lg border border-border bg-card p-2 shadow-dropdown">
 <a href="#" class="block px-3 py-2 text-sm text-foreground">Profile</a>
 <a href="#" class="block px-3 py-2 text-sm text-foreground">Log out</a>
</div>

Tabs

Use role="tablist", role="tab", and role="tabpanel" for custom tab implementations. Use aria-selected for the active tab and support arrow-key navigation:

<div role="tablist" aria-label="Billing sections"
     class="flex gap-2 border-b border-border">
 <button role="tab" aria-selected="true" aria-controls="invoices-panel"
     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">
  Invoices</button>
 <button role="tab" aria-selected="false" aria-controls="payments-panel"
     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">
  Payments</button>
</div>
<div role="tabpanel" id="invoices-panel" class="mt-4">
 <p class="text-sm text-muted-foreground">Invoice history.</p>
</div>
<div role="tabpanel" id="payments-panel" hidden class="mt-4">
 <p class="text-sm text-muted-foreground">Payment methods.</p>
</div>

Toasts and alerts

Use aria-live="polite" for normal updates and aria-live="assertive" only for urgent errors. Avoid repeating the same announcement:

<div id="toast-announcements" class="sr-only" aria-live="polite" aria-atomic="true"></div>

Tooltips

Tooltips should supplement, not replace, labels. Use aria-describedby to connect the trigger with tooltip content. Do not put critical information only inside a hover-only tooltip.

Practical examples

<!-- Toast announcement -->
<div aria-live="polite" class="sr-only" id="toast-announcements">
 <!-- Toast text inserted here dynamically -->
</div>

<!-- Visually hidden but screen-reader visible -->
<button type="button" aria-label="Open user menu"
    class="rounded-button bg-background px-2 py-2">
 <i class="ti ti-dots-vertical" aria-hidden="true"></i>
</button>

Best practices

  • Prefer visible text labels when possible
  • Use aria-label only when visible text is not available
  • Use aria-live="polite" for non-urgent updates
  • Use aria-live="assertive" only for critical errors
  • Hide decorative icons with aria-hidden="true"
  • Keep headings in logical order
  • Test dynamic components with keyboard and at least one screen reader

Common mistakes

Don't

  • Use display: none on text that should be available to screen readers
  • Add aria-live to content that updates every second
  • Use aria-label that conflicts with visible text
  • Hide focusable elements with aria-hidden="true"
  • Put critical instructions only in placeholder text
  • Use <div onclick> instead of <button>

Do

  • Use .sr-only for supplementary screen-reader text
  • Connect errors with aria-describedby
  • Update aria-expanded when dropdowns open or close
  • Use aria-hidden="true" for decorative icons
  • Restore focus after closing modals

Testing checklist

  • Can every control be reached with keyboard?
  • Does each control have a useful accessible name?
  • Does the screen reader announce role and state?
  • Are expanded, selected, checked, disabled, invalid, and busy states announced?
  • When a modal opens, does focus move inside it?
  • When a modal closes, does focus return to the trigger?
  • Are form errors connected to fields?
  • Are live announcements useful and not repeated?
  • Are decorative icons hidden from assistive technology?
  • Does the page still make sense without visual layout?

Developer tips

• VoiceOver on Mac: Cmd+F5 to enable. Navigate with VO+Arrow keys. • NVDA on Windows is a free screen reader for practical testing. • TalkBack on Android for mobile screen-reader testing. • Chrome DevTools Accessibility pane shows the computed accessibility tree and accessible names. • Byteion toasts announce through aria-live="polite" by default. • Test with a real screen reader — automated tools cannot fully validate whether announcements make sense.

Unlock Premium Patterns

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

Explore Pro Components