Skip to main content
Copy-first patterns — combine with form validation CSS and Velin components (velin-dialog, velin-tabs).

Accessibility patterns

Minimal HTML snippets for WCAG-aligned structure. These are not a substitute for manual testing with assistive technologies.

Skip link and focus

VelinStyle ships .velin-skip-link and focus-visible styles in the core bundle. Target your main landmark with href="#main".

<a href="#main" class="velin-skip-link">Skip to main content</a>
…
<main id="main">…</main>

Live regions

Use role="status" + aria-live="polite" for non-urgent updates; role="alert" (assertive) sparingly for errors that must interrupt.

<div id="save-status" role="status" aria-live="polite" class="velin-text-sm velin-text-muted"></div>
<!-- optional: .velin-live-region for visually hidden SR-only updates -->

Dialog

For behavior (focus trap, Esc), use <velin-dialog> or follow the WAI-ARIA dialog pattern. Markup should include aria-modal="true" and label/description references.

<div class="velin-modal-overlay" data-velin-open role="presentation">
  <div class="velin-modal" role="dialog" aria-modal="true"
       aria-labelledby="dlg-title" aria-describedby="dlg-desc">
    <h2 id="dlg-title" class="velin-modal__title">Title</h2>
    <div id="dlg-desc" class="velin-modal__body">…</div>
  </div>
</div>

Tabs

Prefer <velin-tabs> for keyboard support. Static structure example:

<div role="tablist" aria-label="Settings">
  <button type="button" role="tab" aria-selected="true" aria-controls="panel-1" id="tab-1">General</button>
  <button type="button" role="tab" aria-selected="false" aria-controls="panel-2" id="tab-2">Security</button>
</div>
<div role="tabpanel" id="panel-1" aria-labelledby="tab-1">…</div>
<div role="tabpanel" id="panel-2" aria-labelledby="tab-2" hidden>…</div>

Landmarks

<body>
  <header>…</header>
  <nav aria-label="Primary">…</nav>
  <main id="main">…</main>
  <aside aria-label="Related">…</aside>
  <footer>…</footer>
</body>

Forms and errors

Associate errors with aria-describedby; set aria-invalid="true" when invalid. Visual styles: .velin-field-error, .velin-form--validated.

<label class="velin-label" for="email">Email</label>
<input id="email" class="velin-input" aria-invalid="true" aria-describedby="email-err">
<p class="velin-field-error" id="email-err">Invalid address.</p>

Disclosure (collapse)

Prefer <velin-collapse> with a <button slot="trigger"> so aria-expanded and aria-controls stay in sync.

<velin-collapse>
  <button slot="trigger" type="button">Show details</button>
  <p>Expandable content…</p>
</velin-collapse>

Data table

Use <th scope="col|row"> and <caption> for data tables. Wrap wide tables in .velin-table-wrapper.

<div class="velin-table-wrapper">
  <table class="velin-table">
    <caption>Sales by region</caption>
    <thead><tr><th scope="col">Region</th><th scope="col">Total</th></tr></thead>
    <tbody>…</tbody>
  </table>
</div>

Loading / busy

Mark loading regions with aria-busy="true" until content is ready. Pair with .velin-skeleton to avoid layout shift.

<section aria-busy="true" aria-live="polite">
  <div class="velin-skeleton velin-skeleton--heading"></div>
  <div class="velin-skeleton velin-skeleton--text"></div>
</section>