agentsclimarketplace

A11y patterns

Skill Mattakushi432/Claude-Code-Skills-Custom-DevTools-Pack/plugins/devtools-pack/skills/a11y-patterns

A curated pack of custom Claude Code skills for developers — installable as a Claude Code plugin marketplace.

Install
npx -y skills add Mattakushi432/Claude-Code-Skills-Custom-DevTools-Pack --skill a11y-patterns

Assembled from the repository path, not quoted from the project. Check it against their README if it does not work.

One thing to look at

  • 0 stars0 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.

What its author says it does

Copied from the file, not written here

When to activate: accessibility, ARIA, WCAG, screen reader, keyboard navigation, focus management, a11y audit

SKILL.md

4.8 KB, as published. Nobody here has run it

Accessibility Patterns

ARIA Roles & Attributes

<!-- Landmark roles -->
<header role="banner">
<nav aria-label="Main navigation">
<main role="main">
<aside aria-label="Related articles">
<footer role="contentinfo">

<!-- Button vs div -->
<!-- WRONG: -->
<div class="btn" onclick="submit()">Submit</div>
<!-- RIGHT: -->
<button type="submit">Submit</button>

<!-- Icon button — always needs label -->
<button aria-label="Close dialog">
  <svg aria-hidden="true" focusable="false">...</svg>
</button>

<!-- Toggle button -->
<button aria-pressed="false" id="theme-toggle">Dark mode</button>

Live Regions

<!-- Announce dynamic content to screen readers -->
<div aria-live="polite" aria-atomic="true" class="sr-only" id="status"></div>

<!-- Urgent announcements (interrupts) -->
<div role="alert">Your session expires in 5 minutes.</div>
function announce(message, urgency = 'polite') {
  const el = document.getElementById('status');
  el.setAttribute('aria-live', urgency);
  el.textContent = '';
  requestAnimationFrame(() => { el.textContent = message; });
}

Keyboard Navigation

// Roving tabindex for widget internals (e.g., toolbar, listbox)
class RovingTabindex {
  constructor(container) {
    this.items = [...container.querySelectorAll('[role="option"]')];
    this.current = 0;
    this.items[0].tabIndex = 0;
    this.items.slice(1).forEach(el => (el.tabIndex = -1));
    container.addEventListener('keydown', this.#onKey.bind(this));
  }

  #onKey(e) {
    const map = { ArrowDown: 1, ArrowUp: -1, Home: -Infinity, End: Infinity };
    if (!(e.key in map)) return;
    e.preventDefault();
    this.current = Math.max(0, Math.min(this.items.length - 1,
      e.key === 'Home' ? 0 : e.key === 'End' ? this.items.length - 1
      : this.current + map[e.key]
    ));
    this.items.forEach((el, i) => (el.tabIndex = i === this.current ? 0 : -1));
    this.items[this.current].focus();
  }
}

Focus Management

// Trap focus in modal
function trapFocus(modal) {
  const focusable = modal.querySelectorAll(
    'a[href], button:not([disabled]), input, select, textarea, [tabindex]:not([tabindex="-1"])'
  );
  const first = focusable[0];
  const last = focusable[focusable.length - 1];

  modal.addEventListener('keydown', e => {
    if (e.key !== 'Tab') return;
    if (e.shiftKey ? document.activeElement === first : document.activeElement === last) {
      e.preventDefault();
      (e.shiftKey ? last : first).focus();
    }
  });
  first.focus();
}

// Restore focus on modal close
let previousFocus;
function openModal(modal) {
  previousFocus = document.activeElement;
  trapFocus(modal);
}
function closeModal(modal) {
  modal.hidden = true;
  previousFocus?.focus();
}

Skip Links

<a href="#main-content" class="skip-link">Skip to main content</a>
<main id="main-content" tabindex="-1">...</main>
.skip-link {
  position: absolute;
  transform: translateY(-100%);
  transition: transform 200ms;
}
.skip-link:focus { transform: translateY(0); }

Forms

<!-- Associate label explicitly -->
<label for="email">Email address</label>
<input id="email" type="email" autocomplete="email"
       aria-required="true" aria-describedby="email-hint email-error">
<p id="email-hint">We'll never share your email.</p>
<p id="email-error" role="alert" hidden>Please enter a valid email.</p>

<!-- Fieldset for grouped controls -->
<fieldset>
  <legend>Preferred contact method</legend>
  <label><input type="radio" name="contact" value="email"> Email</label>
  <label><input type="radio" name="contact" value="phone"> Phone</label>
</fieldset>

Color Contrast

/* WCAG AA: 4.5:1 for normal text, 3:1 for large text */
/* Use oklch for perceptually uniform color adjustments */
:root {
  --text-on-surface: oklch(15% 0 0);      /* ~12:1 on white */
  --text-muted: oklch(45% 0 0);           /* ~4.6:1 on white — passes AA */
  --color-primary: oklch(40% 0.22 260);   /* ensure contrast on both themes */
}

Reduced Motion

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

axe-core Integration

// In development only
if (process.env.NODE_ENV === 'development') {
  import('axe-core').then(({ default: axe }) => {
    axe.run().then(({ violations }) => {
      violations.forEach(v => console.error(`[a11y] ${v.impact}: ${v.description}`, v.nodes));
    });
  });
}

Keep looking

Skills are one crate of 328,083. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.