agentsclimarketplace

React accessibility

Skill Mattakushi432/Claude-Code-Skills-Custom-DevTools-Pack/plugins/devtools-pack/skills/react-accessibility

When to activate: ARIA, keyboard navigation, focus management, screen readers, accessible forms, skip links, live regionsFrom its SKILL.md

Install
npx -y skills add Mattakushi432/Claude-Code-Skills-Custom-DevTools-Pack --skill react-accessibility

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

5.9 KB, ~1.4k tokens by cl100k_base, as published. Nobody here has run it

React Accessibility Patterns

Semantic HTML First

// BAD: div soup
<div onClick={submit} className="button">Submit</div>

// GOOD: semantic element
<button type="submit" onClick={submit}>Submit</button>

// Landmark regions
<header>
  <nav aria-label="Main navigation">...</nav>
</header>
<main id="main-content">
  <h1>Page Title</h1>
</main>
<aside aria-label="Related articles">...</aside>
<footer>...</footer>

ARIA Attributes

// Interactive state
<button
  aria-expanded={isOpen}
  aria-controls="dropdown-menu"
  aria-haspopup="listbox"
>
  Options
</button>
<ul id="dropdown-menu" role="listbox" aria-label="Options">
  <li role="option" aria-selected={selected === 'a'}>Option A</li>
</ul>

// Live regions — announce dynamic content
<div role="status" aria-live="polite" aria-atomic>
  {statusMessage}
</div>
<div role="alert" aria-live="assertive">
  {errorMessage}
</div>

// Loading states
<div aria-busy={isLoading} aria-label={isLoading ? 'Loading users' : 'Users list'}>
  {isLoading ? <Spinner /> : <UserList />}
</div>

// Visually hidden but accessible
<span className="sr-only">Loading, please wait</span>

Accessible Forms

function AccessibleForm() {
  const emailId = useId()
  const passwordId = useId()
  const emailErrorId = useId()

  return (
    <form noValidate>
      <div>
        <label htmlFor={emailId}>
          Email <span aria-hidden>*</span>
          <span className="sr-only">(required)</span>
        </label>
        <input
          id={emailId}
          type="email"
          required
          aria-required
          aria-invalid={!!emailError}
          aria-describedby={emailError ? emailErrorId : undefined}
          autoComplete="email"
        />
        {emailError && (
          <span id={emailErrorId} role="alert">{emailError}</span>
        )}
      </div>

      <fieldset>
        <legend>Notification preferences</legend>
        <label><input type="checkbox" name="email-notifs" /> Email</label>
        <label><input type="checkbox" name="push-notifs" />  Push</label>
      </fieldset>

      <button type="submit">Submit</button>
    </form>
  )
}

Focus Management

// Return focus on dialog close
function Modal({ isOpen, onClose, children }: ModalProps) {
  const triggerRef = useRef<HTMLElement | null>(null)
  const firstFocusableRef = useRef<HTMLButtonElement>(null)

  useEffect(() => {
    if (isOpen) {
      triggerRef.current = document.activeElement as HTMLElement
      firstFocusableRef.current?.focus()
    } else {
      triggerRef.current?.focus()
    }
  }, [isOpen])

  if (!isOpen) return null

  return createPortal(
    <div role="dialog" aria-modal aria-labelledby="dialog-title">
      <button ref={firstFocusableRef} onClick={onClose} aria-label="Close dialog">
        ×
      </button>
      <h2 id="dialog-title">Dialog Title</h2>
      {children}
    </div>,
    document.body
  )
}

// Focus trap inside modal
function useFocusTrap(containerRef: RefObject<HTMLElement>, active: boolean) {
  useEffect(() => {
    if (!active || !containerRef.current) return
    const el = containerRef.current
    const focusable = el.querySelectorAll<HTMLElement>(
      'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
    )
    const first = focusable[0]
    const last  = focusable[focusable.length - 1]

    const trap = (e: KeyboardEvent) => {
      if (e.key !== 'Tab') return
      if (e.shiftKey && document.activeElement === first) {
        e.preventDefault(); last.focus()
      } else if (!e.shiftKey && document.activeElement === last) {
        e.preventDefault(); first.focus()
      }
    }

    el.addEventListener('keydown', trap)
    return () => el.removeEventListener('keydown', trap)
  }, [active, containerRef])
}

Skip Link

// Place as very first element in <body>
function SkipLink() {
  return (
    <a
      href="#main-content"
      className="sr-only focus:not-sr-only focus:absolute focus:top-4 focus:left-4 focus:z-50 focus:bg-white focus:px-4 focus:py-2"
    >
      Skip to main content
    </a>
  )
}

Keyboard Navigation

// Custom listbox with arrow key navigation
function ListBox({ options, value, onChange }: ListBoxProps) {
  const [activeIdx, setActiveIdx] = useState(0)

  const handleKeyDown = (e: KeyboardEvent<HTMLUListElement>) => {
    switch (e.key) {
      case 'ArrowDown':
        e.preventDefault()
        setActiveIdx(i => Math.min(i + 1, options.length - 1))
        break
      case 'ArrowUp':
        e.preventDefault()
        setActiveIdx(i => Math.max(i - 1, 0))
        break
      case 'Enter':
      case ' ':
        e.preventDefault()
        onChange(options[activeIdx].value)
        break
      case 'Escape':
        // close listbox
        break
    }
  }

  return (
    <ul
      role="listbox"
      tabIndex={0}
      aria-activedescendant={`option-${activeIdx}`}
      onKeyDown={handleKeyDown}
    >
      {options.map((opt, i) => (
        <li
          key={opt.value}
          id={`option-${i}`}
          role="option"
          aria-selected={value === opt.value}
          onClick={() => onChange(opt.value)}
        >
          {opt.label}
        </li>
      ))}
    </ul>
  )
}

Reduced Motion

function useReducedMotion() {
  return useMediaQuery('(prefers-reduced-motion: reduce)')
}

function AnimatedBanner() {
  const reducedMotion = useReducedMotion()

  return (
    <div
      style={{
        animation: reducedMotion
          ? 'none'
          : 'slide-in 0.3s ease-out',
      }}
    >
      Banner content
    </div>
  )
}

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.