SPB Git

spb/ultra-sharp-agent-skills Public

Ultra-Sharp Agent Skills — a research-first skill-authoring system + 72 production-ready skills for AI agents.

Python 100%

# Accessibility Patterns — Copy-Paste Reference

# Contents

  • Skip link
  • Focus styles
  • Accessible form field with error
  • Icon-only button
  • Live regions (async updates)
  • Accessible modal dialog
  • Reduced motion
  • Contrast tokens
  • Gotchas

First focusable element on the page; visible only on focus.

html
<a class="skip-link" href="#main">Skip to main content</a>
<main id="main" tabindex="-1">…</main>
css
.skip-link {
  position: absolute;
  left: -9999px;
}
.skip-link:focus {
  left: 8px;
  top: 8px;
  z-index: 100;
}

# Focus styles

:focus-visible shows the ring for keyboard users without flashing it on every mouse click.

css
:focus-visible {
  outline: 2px solid var(--color-focus, #1a56db);
  outline-offset: 2px;
}
/* never do: :focus { outline: none; } without a replacement */

# Accessible form field with error

html
<label for="email">Email address</label>
<input id="email" name="email" type="email" autocomplete="email"
       aria-describedby="email-error" aria-invalid="true">
<p id="email-error" class="error">Enter an email address with an @, like name@example.com.</p>

Remove aria-invalid and the error node (or empty it) once the field validates.

# Icon-only button

html
<button type="button" aria-label="Close dialog">
  <svg aria-hidden="true" focusable="false">…</svg>
</button>

aria-hidden on the SVG keeps screen readers from announcing the icon twice.

# Live regions (async updates)

html
<!-- polite: status updates, search-result counts -->
<div aria-live="polite" class="visually-hidden" id="status"></div>

<!-- assertive, errors only -->
<div role="alert" id="form-alert"></div>
js
document.getElementById('status').textContent = '12 results found';

The live region must exist in the DOM before you write into it — injecting a new aria-live node with text is not announced reliably.

# Accessible modal dialog

Native <dialog> gives focus trapping and Esc for free — prefer it.

html
<dialog id="confirm" aria-labelledby="confirm-title">
  <h2 id="confirm-title">Delete file?</h2>
  <button type="button" id="cancel">Cancel</button>
  <button type="button" id="ok">Delete</button>
</dialog>
js
const dlg = document.getElementById('confirm');
dlg.showModal();                       // traps focus, Esc closes
dlg.addEventListener('close', () => opener.focus()); // return focus to the trigger

# Reduced motion

css
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }
}

# Contrast tokens

Bake compliance into tokens so components can't ship a failing pair.

css
:root {
  --text-on-light: #1f2937;  /* 14.7:1 on #ffffff */
  --text-muted:    #4b5563;  /* 7.6:1  on #ffffff — still AA for body text */
  --color-focus:   #1a56db;  /* 3.6:1 vs #ffffff — passes 3:1 UI minimum */
}

Check ratios with a tool (e.g., npx wcag-contrast 4b5563 ffffff) rather than by eye.

# Gotchas

  • display: none and visibility: hidden hide content from screen readers too; use a .visually-hidden clip-pattern class for screen-reader-only text.
  • tabindex values >0 break natural focus order — only ever use 0 and -1.
  • role="button" on a div does NOT add Enter/Space handling; you must write the keydown handler yourself (another reason to use <button>).
  • Placeholder text is not a label and usually fails contrast; see designing-forms.
  • aria-label overrides visible text — if a button says "Save" but aria-label="Submit", voice-control users saying "click Save" fail (WCAG 2.5.3 label-in-name).
  • Automated tools (axe, Lighthouse) catch roughly a third of WCAG issues; the keyboard and screen-reader walkthroughs are not optional.