Skip to content

Pattern Accessibility Contracts

Each pattern in Airframe has an accessibility contract - what we guarantee when you use it as documented.

Element: <button class="af-btn"> or <a class="af-btn">

  • Native <button> elements, not divs with roles
  • Proper type attributes (button, submit, reset)
  • Disabled state via disabled attribute on <button>, aria-disabled="true" on <a>
  • Loading state keeps accessible name, uses aria-busy="true"
  • Visible focus via :focus-visible

Element: Native <input>, <select>, <textarea> with .af-input

  • Labels associated via <label for> or wrapping
  • Invalid state: aria-invalid="true"
  • Help/error text linked via aria-describedby
  • Disabled uses native disabled
  • Focus ring visible

Element: Native <input type="checkbox">, <input type="radio">

  • Native inputs styled, never replaced with divs
  • Label association required
  • Grouping uses <fieldset> and <legend>

Element: <input type="checkbox" role="switch">

  • Checkbox semantics with role="switch"
  • aria-checked state
  • Space toggles

Element: <form> with .af-form-* layout classes

  • <label> elements with for attributes or wrapping
  • Error messages with aria-describedby and aria-invalid="true"
  • Required fields with aria-required="true"
  • Help text linked via aria-describedby
  • Field grouping with <fieldset> and <legend>
  • Required indicators are never color-only

Element: <nav class="af-navbar">

  • Semantic <nav> element
  • Active page marked with aria-current="page"
  • Breadcrumbs use proper <nav> + <ol> structure
  • Skip links via af-sr-only-focusable

Element: <dialog class="af-dialog">

  • Native <dialog> element opened with showModal()
  • Accessible name via aria-label or aria-labelledby
  • Page content marked inert when open (native behavior)
  • Escape closes
  • Backdrop click closes
  • Focus trap and focus restoration require supplemental JS for full cross-browser reliability

Element: <dialog class="af-drawer">

  • Same contract as dialogs (treated as modal)
  • aria-modal="true" with dialog role
  • Body scroll lock when open
  • Escape closes, backdrop click closes
  • Focus trap and focus restoration require supplemental JS (same as dialog)

Element: <table class="af-table">

  • Semantic <table>, <thead>, <tbody> structure
  • <caption> for table descriptions
  • <th> with scope="col" or scope="row"
  • No “div table” patterns

Element: .af-callout

  • Status messages: role="status" (polite)
  • Errors/urgent: role="alert" (assertive)
  • Decorative icons use aria-hidden="true"

Element: <details> / <summary>

  • Native <details> and <summary> elements
  • <summary> is keyboard focusable (native)
  • Focus styles on <summary>
  • Disclosure marker styled, not removed

Element: .af-avatar

  • Informative avatars: <img alt="Name">
  • Decorative avatars: alt="" or aria-hidden="true"
  • No essential info in background images

Element: .af-skeleton, .af-spinner

  • Skeleton containers: aria-busy="true" while loading
  • Decorative spinners: aria-hidden="true"
  • Standalone spinners: provide “Loading” text via aria-label or af-sr-only
PatternKeys
ButtonsEnter, Space activate
DialogsEscape closes, Tab cycles within (supplemental JS recommended)
DrawersEscape closes, Tab cycles within (supplemental JS recommended)
FormsStandard form navigation
AccordionEnter, Space toggle on <summary>
PatternRequired attributes
Dialogsaria-modal, aria-labelledby or aria-label
Navigationaria-current="page" on active link
Formsaria-invalid, aria-describedby, aria-required
Alertsrole="status" or role="alert"
Loadingaria-busy="true" on container
Switchrole="switch", aria-checked