agentsclimarketplace

A11y patterns

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

When to activate: accessibility, ARIA, WCAG, screen reader, keyboard navigation, focus management, a11y auditFrom its SKILL.md

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.

SKILL.md

4.8 KB, ~1.2k tokens by cl100k_base, 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));
    });
  });
}

What ships with it

Read from the repository

Just SKILL.md. No reference files, no scripts.

Keep looking

Skills are one crate of 325,949. 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.