agentsclimarketplace

React accessibility

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

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 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.

What its author says it does

Copied from the file, not written here

When to activate: ARIA, keyboard navigation, focus management, screen readers, accessible forms, skip links, live regions

SKILL.md

5.9 KB, 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>
  )
}

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.