Skip to content
cccskills
FreePublished on GitHub

a11y-specialist

Expert in web accessibility (WCAG 2.1/2.2 AA/AAA compliance), ARIA patterns, keyboard navigation, screen reader testing, color contrast, focus management, and automated accessibility testing

The English summary is being prepared. Showing the original description.

See how to install

Files included (2)

  • SKILL.md19.9 KB
  • source.json704 B

SKILL.md (original)

Review the instructions the agent will receive before you install.

Accessibility Specialist

Expert skill for building accessible web applications that comply with WCAG 2.1/2.2 standards. Specializes in ARIA patterns, keyboard navigation, screen reader optimization, automated testing, and inclusive design principles.

Core Capabilities

1. WCAG Compliance

  • Level A: Basic accessibility (must have)
  • Level AA: Mid-level accessibility (should have) - Industry standard
  • Level AAA: Enhanced accessibility (nice to have)
  • WCAG 2.1: Mobile, low vision, cognitive additions
  • WCAG 2.2: Latest standards (focus appearance, dragging)
  • Section 508: US federal requirements
  • ADA: Americans with Disabilities Act compliance

2. ARIA Patterns

  • Roles: Button, dialog, menu, tablist, combobox, listbox
  • States: aria-expanded, aria-selected, aria-checked, aria-pressed
  • Properties: aria-label, aria-labelledby, aria-describedby
  • Live Regions: aria-live, aria-atomic, aria-relevant
  • Relationships: aria-owns, aria-controls, aria-flowto
  • Patterns: WAI-ARIA Authoring Practices Guide (APG)

3. Keyboard Navigation

  • Tab Order: Logical focus order
  • Focus Management: Focus trap, focus restoration
  • Keyboard Shortcuts: Arrow keys, Enter, Space, Escape
  • Skip Links: Skip to main content
  • Focus Indicators: Visible focus styles
  • Roving Tabindex: Composite widgets

4. Screen Reader Support

  • Semantic HTML: Use correct elements
  • Alt Text: Descriptive image alternatives
  • Heading Structure: Logical h1-h6 hierarchy
  • Landmark Regions: header, nav, main, aside, footer
  • Announcements: Dynamic content updates
  • Hidden Content: aria-hidden, sr-only classes

5. Visual Accessibility

  • Color Contrast: WCAG AA (4.5:1 text, 3:1 UI)
  • Color Independence: Don't rely on color alone
  • Text Sizing: Relative units (rem, em)
  • Spacing: Touch targets 44×44px minimum
  • Motion: Respect prefers-reduced-motion
  • Zoom: Support 200% zoom

6. Automated Testing

  • axe-core: Industry standard accessibility engine
  • jest-axe: Jest integration for unit tests
  • Lighthouse: Google accessibility audits
  • Pa11y: CI/CD accessibility testing
  • Testing Library: Built-in accessibility queries
  • ESLint: jsx-a11y plugin

7. Manual Testing

  • Keyboard Only: Test without mouse
  • Screen Readers: NVDA, JAWS, VoiceOver
  • Browser DevTools: Accessibility tree inspection
  • Color Blindness: Simulators
  • Zoom Testing: 200% zoom verification
  • User Testing: Real users with disabilities

Workflow

Phase 1: Accessibility Planning

  1. Define Requirements

    • WCAG level target (A, AA, AAA)?
    • Legal requirements (Section 508, ADA)?
    • User personas with disabilities?
    • Priority features for accessibility?
  2. Audit Existing Code

    • Run automated tools (axe, Lighthouse)
    • Manual keyboard testing
    • Screen reader testing
    • Color contrast checks
  3. Create Action Plan

    • Categorize issues (critical, high, medium, low)
    • Estimate effort
    • Prioritize fixes
    • Set timeline

Phase 2: Implementation

  1. Semantic HTML

    • Use correct elements
    • Add ARIA only when needed
    • Structure content logically
    • Use landmarks
  2. Keyboard Support

    • Add keyboard handlers
    • Manage focus
    • Implement roving tabindex
    • Add skip links
  3. Screen Reader Support

    • Add ARIA labels
    • Implement live regions
    • Test with real screen readers
    • Fix announced text
  4. Visual Accessibility

    • Fix color contrast
    • Add focus indicators
    • Ensure text sizing
    • Test with zoom

Phase 3: Testing & Maintenance

  1. Automated Testing

    • Unit tests with jest-axe
    • Integration tests
    • CI/CD pipeline checks
    • Regular audits
  2. Manual Testing

    • Keyboard navigation
    • Screen reader testing
    • User testing
    • Browser compatibility
  3. Documentation

    • Accessibility statement
    • Known issues
    • Usage guidelines
    • Testing procedures

Accessibility Patterns

Accessible Button

// AccessibleButton.tsx
import { forwardRef, ButtonHTMLAttributes } from 'react'

interface AccessibleButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
  /**
   * Accessible label for screen readers
   * Required if button contains only an icon
   */
  'aria-label'?: string

  /**
   * ID of element that labels this button
   */
  'aria-labelledby'?: string

  /**
   * ID of element that describes this button
   */
  'aria-describedby'?: string

  /**
   * Loading state (shows spinner, disables interaction)
   */
  loading?: boolean
}

export const AccessibleButton = forwardRef<HTMLButtonElement, AccessibleButtonProps>(
  ({ children, loading, disabled, 'aria-label': ariaLabel, ...props }, ref) => {
    return (
      <button
        ref={ref}
        type="button"
        disabled={disabled || loading}
        aria-label={ariaLabel}
        aria-busy={loading}
        aria-disabled={disabled || loading}
        {...props}
      >
        {loading && (
          <span className="spinner" aria-hidden="true">
            {/* Spinner icon */}
          </span>
        )}
        {children}
        {loading && <span className="sr-only">Loading...</span>}
      </button>
    )
  }
)

AccessibleButton.displayName = 'AccessibleButton'

// Usage
<AccessibleButton onClick={handleClick}>
  Save Changes
</AccessibleButton>

// Icon-only button (MUST have aria-label)
<AccessibleButton aria-label="Close dialog" onClick={onClose}>
  <XIcon aria-hidden="true" />
</AccessibleButton>

Accessible Modal/Dialog

// AccessibleModal.tsx
import { useEffect, useRef, ReactNode } from 'react'
import { createPortal } from 'react-dom'
import { useFocusTrap } from './hooks/useFocusTrap'
import { useEscapeKey } from './hooks/useEscapeKey'

interface AccessibleModalProps {
  isOpen: boolean
  onClose: () => void
  title: string
  children: ReactNode
  /**
   * ID for the modal title (for aria-labelledby)
   */
  titleId?: string
  /**
   * ID for the modal description (for aria-describedby)
   */
  descriptionId?: string
}

export function AccessibleModal({
  isOpen,
  onClose,
  title,
  children,
  titleId = 'modal-title',
  descriptionId,
}: AccessibleModalProps) {
  const modalRef = useRef<HTMLDivElement>(null)
  const previousActiveElement = useRef<HTMLElement | null>(null)

  // Trap focus inside modal
  useFocusTrap(modalRef, isOpen)

  // Close on Escape
  useEscapeKey(onClose, isOpen)

  useEffect(() => {
    if (isOpen) {
      // Store currently focused element
      previousActiveElement.current = document.activeElement as HTMLElement

      // Prevent body scroll
      document.body.style.overflow = 'hidden'

      // Focus modal
      modalRef.current?.focus()
    } else {
      // Restore body scroll
      document.body.style.overflow = ''

      // Restore focus to previous element
      previousActiveElement.current?.focus()
    }

    return () => {
      document.body.style.overflow = ''
    }
  }, [isOpen])

  if (!isOpen) return null

  return createPortal(
    <div
      className="modal-overlay"
      onClick={onClose}
      role="presentation"
    >
      <div
        ref={modalRef}
        role="dialog"
        aria-modal="true"
        aria-labelledby={titleId}
        aria-describedby={descriptionId}
        className="modal-content"
        onClick={(e) => e.stopPropagation()}
        tabIndex={-1}
      >
        <div className="modal-header">
          <h2 id={titleId}>{title}</h2>
          <button
            onClick={onClose}
            aria-label="Close dialog"
            className="modal-close"
          >
            <span aria-hidden="true">×</span>
          </button>
        </div>

        <div className="modal-body" id={descriptionId}>
          {children}
        </div>
      </div>
    </div>,
    document.body
  )
}

// Usage
<AccessibleModal
  isOpen={isOpen}
  onClose={() => setIsOpen(false)}
  title="Confirm Action"
  descriptionId="modal-desc"
>
  <p id="modal-desc">Are you sure you want to delete this item?</p>
  <button onClick={handleConfirm}>Confirm</button>
  <button onClick={() => setIsOpen(false)}>Cancel</button>
</AccessibleModal>

Focus Trap Hook

// hooks/useFocusTrap.ts
import { useEffect, RefObject } from 'react'

const FOCUSABLE_ELEMENTS = [
  'a[href]',
  'button:not([disabled])',
  'textarea:not([disabled])',
  'input:not([disabled])',
  'select:not([disabled])',
  '[tabindex]:not([tabindex="-1"])',
].join(', ')

export function useFocusTrap(ref: RefObject<HTMLElement>, isActive: boolean) {
  useEffect(() => {
    if (!isActive) return

    const element = ref.current
    if (!element) return

    const focusableElements = element.querySelectorAll<HTMLElement>(FOCUSABLE_ELEMENTS)
    const firstFocusable = focusableElements[0]
    const lastFocusable = focusableElements[focusableElements.length - 1]

    const handleTabKey = (e: KeyboardEvent) => {
      if (e.key !== 'Tab') return

      if (e.shiftKey) {
        // Shift + Tab
        if (document.activeElement === firstFocusable) {
          lastFocusable?.focus()
          e.preventDefault()
        }
      } else {
        // Tab
        if (document.activeElement === lastFocusable) {
          firstFocusable?.focus()
          e.preventDefault()
        }
      }
    }

    element.addEventListener('keydown', handleTabKey)

    return () => {
      element.removeEventListener('keydown', handleTabKey)
    }
  }, [ref, isActive])
}

Accessible Form with Live Validation

// AccessibleForm.tsx
import { useState, useId, FormEvent } from 'react'

export function AccessibleForm() {
  const [email, setEmail] = useState('')
  const [error, setError] = useState('')
  const [success, setSuccess] = useState('')

  const emailId = useId()
  const errorId = useId()
  const successId = useId()

  const validateEmail = (value: string) => {
    if (!value) {
      setError('Email is required')
      return false
    }
    if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) {
      setError('Please enter a valid email address')
      return false
    }
    setError('')
    return true
  }

  const handleSubmit = (e: FormEvent) => {
    e.preventDefault()
    if (validateEmail(email)) {
      setSuccess('Form submitted successfully!')
      // Submit form
    }
  }

  return (
    <form onSubmit={handleSubmit} noValidate>
      <div className="form-field">
        <label htmlFor={emailId}>
          Email Address <span aria-label="required">*</span>
        </label>

        <input
          id={emailId}
          type="email"
          value={email}
          onChange={(e) => setEmail(e.target.value)}
          onBlur={() => validateEmail(email)}
          aria-invalid={!!error}
          aria-describedby={error ? errorId : undefined}
          aria-required="true"
          autoComplete="email"
        />

        {error && (
          <div
            id={errorId}
            role="alert"
            aria-live="polite"
            className="error-message"
          >
            {error}
          </div>
        )}
      </div>

      {success && (
        <div
          id={successId}
          role="status"
          aria-live="polite"
          className="success-message"
        >
          {success}
        </div>
      )}

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

Accessible Tabs

// AccessibleTabs.tsx
import { useState, useRef, useEffect, KeyboardEvent } from 'react'

interface Tab {
  id: string
  label: string
  content: React.ReactNode
}

interface AccessibleTabsProps {
  tabs: Tab[]
  defaultTab?: string
}

export function AccessibleTabs({ tabs, defaultTab }: AccessibleTabsProps) {
  const [activeTab, setActiveTab] = useState(defaultTab || tabs[0].id)
  const tabRefs = useRef<Map<string, HTMLButtonElement>>(new Map())

  const handleKeyDown = (e: KeyboardEvent, currentIndex: number) => {
    let newIndex = currentIndex

    switch (e.key) {
      case 'ArrowLeft':
        newIndex = currentIndex > 0 ? currentIndex - 1 : tabs.length - 1
        break
      case 'ArrowRight':
        newIndex = currentIndex < tabs.length - 1 ? currentIndex + 1 : 0
        break
      case 'Home':
        newIndex = 0
        break
      case 'End':
        newIndex = tabs.length - 1
        break
      default:
        return
    }

    e.preventDefault()
    const newTab = tabs[newIndex]
    setActiveTab(newTab.id)
    tabRefs.current.get(newTab.id)?.focus()
  }

  return (
    <div className="tabs">
      {/* Tab List */}
      <div role="tablist" aria-label="Content tabs">
        {tabs.map((tab, index) => {
          const isActive = activeTab === tab.id

          return (
            <button
              key={tab.id}
              ref={(el) => {
                if (el) tabRefs.current.set(tab.id, el)
              }}
              role="tab"
              id={`tab-${tab.id}`}
              aria-selected={isActive}
              aria-controls={`panel-${tab.id}`}
              tabIndex={isActive ? 0 : -1}
              onClick={() => setActiveTab(tab.id)}
              onKeyDown={(e) => handleKeyDown(e, index)}
              className={isActive ? 'active' : ''}
            >
              {tab.label}
            </button>
          )
        })}
      </div>

      {/* Tab Panels */}
      {tabs.map((tab) => (
        <div
          key={tab.id}
          role="tabpanel"
          id={`panel-${tab.id}`}
          aria-labelledby={`tab-${tab.id}`}
          hidden={activeTab !== tab.id}
          tabIndex={0}
        >
          {tab.content}
        </div>
      ))}
    </div>
  )
}

Screen Reader Only Text

/* sr-only.css */
.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border-width: 0;
}

.sr-only-focusable:focus,
.sr-only-focusable:active {
  position: static;
  width: auto;
  height: auto;
  overflow: visible;
  clip: auto;
  white-space: normal;
}
// Usage
<button>
  <TrashIcon aria-hidden="true" />
  <span className="sr-only">Delete item</span>
</button>

Skip Links

// SkipLinks.tsx
export function SkipLinks() {
  return (
    <div className="skip-links">
      <a href="#main-content" className="skip-link">
        Skip to main content
      </a>
      <a href="#navigation" className="skip-link">
        Skip to navigation
      </a>
      <a href="#footer" className="skip-link">
        Skip to footer
      </a>
    </div>
  )
}
/* Skip link styles */
.skip-link {
  position: absolute;
  top: -40px;
  left: 0;
  background: #000;
  color: #fff;
  padding: 8px;
  text-decoration: none;
  z-index: 100;
}

.skip-link:focus {
  top: 0;
}

Automated Testing

Jest + jest-axe

// Button.test.tsx
import { render } from '@testing-library/react'
import { axe, toHaveNoViolations } from 'jest-axe'
import { Button } from './Button'

expect.extend(toHaveNoViolations)

describe('Button Accessibility', () => {
  it('should not have accessibility violations', async () => {
    const { container } = render(<Button>Click me</Button>)
    const results = await axe(container)
    expect(results).toHaveNoViolations()
  })

  it('should have accessible name', () => {
    const { getByRole } = render(<Button>Click me</Button>)
    expect(getByRole('button', { name: 'Click me' })).toBeInTheDocument()
  })

  it('icon-only button should have aria-label', async () => {
    const { container } = render(
      <Button aria-label="Close">
        <XIcon />
      </Button>
    )
    const results = await axe(container)
    expect(results).toHaveNoViolations()
  })

  it('disabled button should have aria-disabled', () => {
    const { getByRole } = render(<Button disabled>Click me</Button>)
    expect(getByRole('button')).toHaveAttribute('aria-disabled', 'true')
  })
})

Testing Library Accessibility Queries

// Form.test.tsx
import { render, screen } from '@testing-library/react'
import { userEvent } from '@testing-library/user-event'

describe('Form Accessibility', () => {
  it('should have accessible form fields', () => {
    render(<LoginForm />)

    // Use accessible queries (getByRole, getByLabelText)
    const emailInput = screen.getByLabelText(/email/i)
    const passwordInput = screen.getByLabelText(/password/i)
    const submitButton = screen.getByRole('button', { name: /log in/i })

    expect(emailInput).toBeInTheDocument()
    expect(passwordInput).toBeInTheDocument()
    expect(submitButton).toBeInTheDocument()
  })

  it('should show validation errors with proper ARIA', async () => {
    const user = userEvent.setup()
    render(<LoginForm />)

    const submitButton = screen.getByRole('button', { name: /log in/i })
    await user.click(submitButton)

    // Error should be announced to screen readers
    const errorAlert = screen.getByRole('alert')
    expect(errorAlert).toHaveTextContent(/email is required/i)
  })
})

Lighthouse CI

# .lighthouserc.yml
ci:
  collect:
    numberOfRuns: 3
    startServerCommand: 'npm run start'
    url:
      - 'http://localhost:3000'
  assert:
    preset: 'lighthouse:recommended'
    assertions:
      # Accessibility score must be >= 90
      'categories:accessibility':
        - error
        - minScore: 0.9

      # Specific accessibility checks
      'aria-allowed-attr': 'error'
      'aria-required-attr': 'error'
      'aria-valid-attr': 'error'
      'button-name': 'error'
      'color-contrast': 'error'
      'document-title': 'error'
      'html-has-lang': 'error'
      'image-alt': 'error'
      'label': 'error'
      'link-name': 'error'

Best Practices

Semantic HTML First

// ❌ BAD - Div soup
<div onClick={handleClick}>Click me</div>

// ✅ GOOD - Use button
<button onClick={handleClick}>Click me</button>

ARIA is a Last Resort

// ❌ BAD - Unnecessary ARIA
<button role="button" aria-label="Submit">Submit</button>

// ✅ GOOD - Native semantics
<button>Submit</button>

Always Provide Text Alternatives

// ❌ BAD - Icon without label
<button><XIcon /></button>

// ✅ GOOD - Icon with label
<button aria-label="Close"><XIcon aria-hidden="true" /></button>

Keyboard Accessibility

// ✅ Ensure all interactive elements are keyboard accessible
<div
  role="button"
  tabIndex={0}
  onClick={handleClick}
  onKeyDown={(e) => {
    if (e.key === 'Enter' || e.key === ' ') {
      e.preventDefault()
      handleClick()
    }
  }}
>
  Custom button
</div>

Focus Management

// ✅ Manage focus in SPAs
useEffect(() => {
  // Focus heading when page changes
  headingRef.current?.focus()
}, [page])

When to Use This Skill

Activate this skill when you need to:

  • Build WCAG compliant components
  • Add ARIA attributes correctly
  • Implement keyboard navigation
  • Create accessible modals/dialogs
  • Fix accessibility issues
  • Set up automated a11y testing
  • Audit accessibility compliance
  • Train team on accessibility
  • Create accessible forms
  • Implement screen reader support

Integration with Agents

// Agent Explore → Scan all components for a11y issues
// a11y-specialist → Apply fixes automatically

// Example workflow:
1. Agent finds: 50 buttons without accessible names
2. a11y-specialist adds aria-label to all
3. Agent verifies: All buttons now accessible

Output Format

When implementing accessibility, provide:

  1. Accessible Component: WCAG compliant code
  2. ARIA Documentation: Explain ARIA usage
  3. Keyboard Support: Document keyboard interactions
  4. Test Suite: jest-axe tests included
  5. Manual Testing Guide: How to test with screen readers
  6. Compliance Notes: WCAG level achieved

Always build interfaces that are usable by everyone, regardless of ability.

Reviews

No reviews yet. Tell others what you thought.

Comments

No login needed. Share how it worked for you, ask a question, or note where it did not run.

No comments yet. Be the first to write one.

Comments are published right away. Do not include personal information such as email addresses or phone numbers. Up to 2 URLs.

0 / 1,000 characters

This form is protected by reCAPTCHA, and the Google Privacy Policy and Terms of Service apply.

More skills from this repository

007

Free

Security audit, hardening, threat modeling (STRIDE/PASTA), Red/Blue Team, OWASP checks, code review, incident response, and infrastructure security for any project.

The English summary is being prepared. Showing the original description.

aibot88/sec_skill_store5Updated May 27, 2026

Guides the creation of agile user stories and Gherkin feature files. Use when the user wants to create a user story, write acceptance criteria, define Gherkin scenarios, or author BDD feature files. This should trigger for requests such as Create a user story; Write a user story; I need to write a user story. Part of cursor-rules-java project

The English summary is being prepared. Showing the original description.

aibot88/sec_skill_store5Updated May 27, 2026

Facilitates conversational discovery to create Architectural Decision Records (ADRs) for non-functional requirements using the ISO/IEC 25010:2023 quality model. Use when the user wants to document quality attributes, NFR decisions, security/performance/scalability architecture, or design systems with measurable quality criteria. This should trigger for requests such as Create ADR for Non-functional requirements; Document Non-functional requirements; Capture Non-functional requirements; Generate Non-functional requirements in an ADR. Part of cursor-rules-java project

The English summary is being prepared. Showing the original description.

aibot88/sec_skill_store5Updated May 27, 2026

Run a health check on an existing project: dependency audit, security scan, test runner detection, CI/CD evaluation, and missing configuration analysis. Maps the three execution gates (pre/in/post) from /10x-bootstrapper to an assessment framework for existing codebases. Reads optional context/foundation/stack-assessment.md from /10x-stack-assess to focus checks on identified gaps. Writes context/foundation/health-check.md with findings, prioritized fixes, and an agent-readiness verdict. Use when the user has an existing project and wants to verify its health before working with an agent. Trigger phrases: "health check", "check my project", "audit my project", "is my project healthy", "sprawdź projekt", "audyt projektu", "health-check", "project health". Use AFTER /10x-stack-assess (brownfield chain), BEFORE agent onboarding (m1-l4).

The English summary is being prepared. Showing the original description.

aibot88/sec_skill_store5Updated May 27, 2026

You MUST use this when building projects end-to-end. Orchestrates all 12 team roles — automatically switches between CTO, architect, PM, engineers, SRE, security, DBA, QA, and EM based on the current phase of work. Starts with brainstorming before any implementation.

The English summary is being prepared. Showing the original description.

aibot88/sec_skill_store5Updated May 27, 2026

Use when you need to add or configure Maven plugins in your pom.xml — including quality tools (enforcer, surefire, failsafe, jacoco, pitest, spotbugs, pmd), security scanning (OWASP), code formatting (Spotless), version management, container image build (Jib), build information tracking, and benchmarking (JMH) — through a consultative, modular step-by-step approach that only adds what you actually need. This should trigger for requests such as Add Maven plugins in pom.xml; Improve Maven plugins in pom.xml. Part of cursor-rules-java project

The English summary is being prepared. Showing the original description.

aibot88/sec_skill_store5Updated May 27, 2026

See all skills by aibot88

Report a problem with this skill