AgentStack
Browse Sign in
Browse Why AgentStack Sell Docs
Sign in
SKILL verified MIT Self-run

Accessibility

skill-fernando-bertholdo-4-successful-ai-life-accessibility · by fernando-bertholdo

Use when building or reviewing UI components to ensure WCAG 2.1 compliance, keyboard navigation, semantic HTML, ARIA attributes, color contrast, and assistive technology support.

No reviews yet
0 installs
35 views
0.0% view→install

Install

$ agentstack add skill-fernando-bertholdo-4-successful-ai-life-accessibility

✓ scanned · ✓ verified, works with Claude Code, Cursor, and more.

Security review

✓ Passed

No issues found. Passed automated security review. · v0.1.0 How review works →

  • Prompt-injection patterns
  • Secret / credential exfiltration
  • Dangerous shell & filesystem operations
  • Untrusted network calls
  • Known-malicious package signatures

What it can access

  • Network access No
  • Filesystem access No
  • Shell / process execution No
  • Environment & secrets No
  • Dynamic code execution No

From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.

View the full security report →

Verified badge

Passed review? Show it. Paste this badge into your README, it links to the public security report.

AgentStack Verified badge Links to your public security report.
[![AgentStack Verified](https://agentstack.voostack.com/badges/verified.svg)](https://agentstack.voostack.com/security/report/skill-fernando-bertholdo-4-successful-ai-life-accessibility)

Reliability & compatibility

Security review passed
0 installs to date
no reviews yet
2mo ago

Declared compatibility

Claude CodeClaude Desktop

Compatibility is declared by the source manifest. End-to-end runtime verification is coming, see below.

Preview Execution monitoring

We're building live execution health for every listing: tool-call success rate, median latency, uptime, and last-checked timestamps, measured, not self-reported. It isn't live yet, so we don't show numbers we can't stand behind.

How agent discovery & health will work →
Are you the author of Accessibility? Claim this listing to set pricing, connect Stripe payouts, and keep 70% of every sale.
Sign up to claim

About

UI Accessibility (WCAG 2.1)

Overview

This skill guides implementation and review of accessible UI components following WCAG 2.1 Level AA standards. Accessibility ensures your interface is usable by everyone—keyboard users, screen reader users, users with low vision, and users with cognitive disabilities.

Core principle: Semantic HTML first, ARIA last. Keyboard navigation and screen reader support are non-negotiable.


When to Use / When NOT to Use

✅ Use This Skill When

  • Building new UI components (buttons, modals, dropdowns, forms, tabs)
  • Adding keyboard navigation or focus management
  • Reviewing code for accessibility violations
  • Testing with screen readers or keyboard-only
  • Ensuring color contrast and visual focus indicators
  • Adding ARIA attributes to dynamic content

❌ Do NOT Use When

  • Component is purely decorative (no interaction needed)
  • Audience explicitly doesn't include users with accessibility needs (internally impossible—always assume diverse users)
  • You're skipping automated testing due to time constraints (violates DoD)

Core Concepts

1. Semantic HTML Structure

Foundation: Use meaningful HTML elements to convey structure and purpose.


  
    
      Home
      About
    
  

  
    Page Title
    
      Section Heading
      Content...
    
  

  © 2026 Company

Main Title
Section
Subsection
Another Section

  
    Home
    About
  

  Page Title

Main Title
Jumps to h3 (breaks hierarchy)

Label Association Rules:


Email Address

  Email Address
  

Email Address

2. Keyboard Navigation

Principle: All interactive features usable via keyboard—Tab, Shift+Tab, Enter, Space, Arrow keys, Escape.

Key Bindings:

  • Tab / Shift+Tab: Move focus forward/backward
  • Enter / Space: Activate buttons, toggle checkboxes
  • Arrow keys: Navigate lists, menus, sliders, tabs (up/down for vertical; left/right for horizontal)
  • Home / End: Jump to first/last item in list
  • Escape: Close modal, dropdown, or menu

Focus Management:


Click me

Skip to main content

First
Second

button:focus {
  outline: none; /* NEVER DO THIS */
}

Skip Links (Focus Management):


Skip to main content

  .skip-link {
    position: absolute;
    left: -9999px;
    z-index: 999;
  }

  .skip-link:focus {
    left: 0;
    top: 0;
    background: #000;
    color: #fff;
    padding: 8px 12px;
  }

  

3. ARIA Attributes

Golden Rule: Semantic HTML first. Use ARIA only when native HTML doesn't exist or to enhance semantics.

Common ARIA Attributes

| Attribute | Purpose | Example | |-----------|---------|---------| | aria-label | Names element (for icons, buttons without text) | × | | aria-labelledby | Links to another element as label | Confirm Delete... | | aria-describedby | Adds descriptive text (error, hint) | Min 8 chars | | aria-live | Announces dynamic content ("polite" or "assertive") | Item added to cart | | aria-hidden | Hides from screen readers (decorative only) | | | role | Semantic role | ... | | aria-expanded | Indicates expanded/collapsed state | Menu | | aria-haspopup | Signals a popup exists | Options | | aria-selected | Current selection in list/tabs | Tab 1 | | aria-modal | Marks modal dialogs | ... | | aria-required | Flags required form fields | ` | | aria-invalid | Indicates validation error | | | aria-atomic | Reads entire content block | Status: Updated` |


4. Color Contrast & Visual Focus

WCAG Contrast Ratios:

| Level | Normal Text | Large Text (18pt+) | |-------|-------------|-------------------| | AA (minimum) | 4.5:1 | 3:1 | | AAA (enhanced) | 7:1 | 4.5:1 |

Visual Focus Indicators (Required):

/* ✅ CORRECT: Visible focus indicator */
button:focus {
  outline: 2px solid #0066cc;
  outline-offset: 2px;
}

/* ✅ CORRECT: Alternative (background change) */
button:focus {
  background-color: #e6f2ff;
  box-shadow: inset 0 0 0 2px #0066cc;
}

/* ❌ WRONG: Removing focus outline */
button:focus {
  outline: none; /* FORBIDDEN */
}

/* ❌ WRONG: Low contrast focus */
button:focus {
  outline: 1px solid #ccc; /* Too subtle */
}

Never Convey Info by Color Alone:


This field is required

  * This field is required

I agree (required)

5. Testing & Validation

Automated Tools:

  • axe DevTools: Browser extension for violations (Chrome, Firefox)
  • Lighthouse: Chrome DevTools → Lighthouse → Accessibility (target: 90+)
  • Pa11y: CLI tool for automated scanning
  • jest-axe: Automated testing in Jest

Manual Testing:

  1. Keyboard-only: Unplug mouse; navigate using Tab, Enter, Space, arrows
  2. Screen readers: NVDA (Windows), JAWS (Windows), VoiceOver (macOS/iOS)
  3. Color contrast: WebAIM Contrast Checker
  4. Zoom: Test at 200% zoom; ensure nothing breaks

Acceptance Criteria:

  • Zero axe DevTools violations
  • Lighthouse Accessibility ≥ 90
  • All features functional via keyboard
  • All images have alt text
  • All form inputs have labels

Component Patterns

Pattern 1: Accessible Dropdown Menu

import { useState, useRef, useEffect } from 'react';

export function AccessibleDropdown() {
  const [isOpen, setIsOpen] = useState(false);
  const [selectedIndex, setSelectedIndex] = useState(0);
  const buttonRef = useRef(null);
  const menuRef = useRef(null);

  const options = ['Edit', 'Duplicate', 'Delete'];

  // Close on Escape
  useEffect(() => {
    const handleKeyDown = (e) => {
      if (e.key === 'Escape' && isOpen) {
        setIsOpen(false);
        buttonRef.current?.focus();
      }
    };
    document.addEventListener('keydown', handleKeyDown);
    return () => document.removeEventListener('keydown', handleKeyDown);
  }, [isOpen]);

  // Focus management for arrow keys
  const handleMenuKeyDown = (e) => {
    switch (e.key) {
      case 'ArrowDown':
        e.preventDefault();
        setSelectedIndex((i) => (i + 1) % options.length);
        break;
      case 'ArrowUp':
        e.preventDefault();
        setSelectedIndex((i) => (i - 1 + options.length) % options.length);
        break;
      case 'Home':
        e.preventDefault();
        setSelectedIndex(0);
        break;
      case 'End':
        e.preventDefault();
        setSelectedIndex(options.length - 1);
        break;
      case 'Enter':
      case ' ':
        e.preventDefault();
        handleSelect(options[selectedIndex]);
        break;
      default:
        break;
    }
  };

  const handleSelect = (option) => {
    console.log('Selected:', option);
    setIsOpen(false);
    buttonRef.current?.focus();
  };

  return (
    
       setIsOpen(!isOpen)}
      >
        Options
      

      {isOpen && (
        
          {options.map((option, i) => (
            
               setSelectedIndex(i)}
                onClick={() => handleSelect(option)}
              >
                {option}
              
            
          ))}
        
      )}
    
  );
}

Pattern 2: Accessible Modal Dialog

import { useEffect, useRef } from 'react';

export function AccessibleModal({ isOpen, onClose, title, children }) {
  const dialogRef = useRef(null);

  useEffect(() => {
    if (!isOpen) return;

    // Store the element that opened the modal for focus restore
    const previouslyFocused = document.activeElement;

    // Focus the modal
    dialogRef.current?.focus();

    // Focus trap: trap focus inside modal
    const handleKeyDown = (e) => {
      if (e.key === 'Escape') {
        onClose();
        previouslyFocused?.focus();
        return;
      }

      // Trap Tab inside modal
      if (e.key === 'Tab') {
        const focusableElements = dialogRef.current?.querySelectorAll(
          'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
        );
        if (!focusableElements || focusableElements.length === 0) return;

        const firstElement = focusableElements[0];
        const lastElement = focusableElements[focusableElements.length - 1];

        if (e.shiftKey) {
          if (document.activeElement === firstElement) {
            e.preventDefault();
            lastElement.focus();
          }
        } else {
          if (document.activeElement === lastElement) {
            e.preventDefault();
            firstElement.focus();
          }
        }
      }
    };

    document.addEventListener('keydown', handleKeyDown);
    return () => {
      document.removeEventListener('keydown', handleKeyDown);
      previouslyFocused?.focus();
    };
  }, [isOpen, onClose]);

  if (!isOpen) return null;

  return (
    <>
      {/* Backdrop */}
      

      {/* Modal */}
      
        {title}
        {children}
        Close
      
    
  );
}

Pattern 3: Accessible Alert/Notification

export function AccessibleAlert({ type, message, onDismiss }) {
  // type: 'error' | 'success' | 'warning' | 'info'
  const isError = type === 'error';

  return (
    
      
        {type === 'error' && '⚠️'}
        {type === 'success' && '✓'}
        {type === 'warning' && '⚡'}
        {type === 'info' && 'ℹ️'}
      
      {message}
      {onDismiss && (
        
          ×
        
      )}
    
  );
}

Pattern 4: Accessible Form with Error Handling

import { useState } from 'react';

export function AccessibleForm() {
  const [formData, setFormData] = useState({ email: '', password: '' });
  const [errors, setErrors] = useState({});

  const validate = () => {
    const newErrors = {};

    if (!formData.email) {
      newErrors.email = 'Email is required';
    } else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(formData.email)) {
      newErrors.email = 'Email must follow format: user@domain.com';
    }

    if (!formData.password) {
      newErrors.password = 'Password is required';
    } else if (formData.password.length  {
    e.preventDefault();
    if (validate()) {
      console.log('Form submitted:', formData);
    }
  };

  return (
    
      {/* Email Field */}
      
        
          Email Address *
        
         setFormData({ ...formData, email: e.target.value })}
          aria-required="true"
          aria-invalid={!!errors.email}
          aria-describedby={errors.email ? 'email-error' : undefined}
        />
        {errors.email && (
          
            {errors.email}
          
        )}
      

      {/* Password Field */}
      
        
          Password *
        
         setFormData({ ...formData, password: e.target.value })}
          aria-required="true"
          aria-invalid={!!errors.password}
          aria-describedby={errors.password ? 'password-error' : 'password-hint'}
        />
        
          Minimum 8 characters
        
        {errors.password && (
          
            {errors.password}
          
        )}
      

      Sign In
    
  );
}

Pattern 5: Accessible Tab Component

import { useState } from 'react';

export function AccessibleTabs() {
  const [activeTab, setActiveTab] = useState(0);

  const tabs = [
    { label: 'Overview', content: 'Overview content here' },
    { label: 'Details', content: 'Details content here' },
    { label: 'Related', content: 'Related items here' },
  ];

  const handleTabKeyDown = (e, index) => {
    let nextIndex = index;

    switch (e.key) {
      case 'ArrowLeft':
        e.preventDefault();
        nextIndex = (index - 1 + tabs.length) % tabs.length;
        break;
      case 'ArrowRight':
        e.preventDefault();
        nextIndex = (index + 1) % tabs.length;
        break;
      case 'Home':
        e.preventDefault();
        nextIndex = 0;
        break;
      case 'End':
        e.preventDefault();
        nextIndex = tabs.length - 1;
        break;
      default:
        return;
    }

    setActiveTab(nextIndex);
    // Focus the newly active tab
    document.getElementById(`tab-${nextIndex}`)?.focus();
  };

  return (
    
      {/* Tablist */}
      
        {tabs.map((tab, i) => (
           setActiveTab(i)}
            onKeyDown={(e) => handleTabKeyDown(e, i)}
          >
            {tab.label}
          
        ))}
      

      {/* Tabpanels */}
      {tabs.map((tab, i) => (
        
          {tab.content}
        
      ))}
    
  );
}

Pattern 6: Testing with axe-core and Jest

import { render, screen } from '@testing-library/react';
import { axe, toHaveNoViolations } from 'jest-axe';
import { AccessibleForm } from './AccessibleForm';

expect.extend(toHaveNoViolations);

describe('Accessibility Tests', () => {
  test('form has no accessibility violations', async () => {
    const { container } = render();
    const results = await axe(container);
    expect(results).toHaveNoViolations();
  });

  test('form is keyboard navigable', () => {
    render();
    const emailInput = screen.getByLabelText('Email Address');
    const passwordInput = screen.getByLabelText('Password');
    const submitButton = screen.getByRole('button', { name: /sign in/i });

    // Tab focus order
    emailInput.focus();
    expect(document.activeElement).toBe(emailInput);

    // Tab to password
    const tabEvent = new KeyboardEvent('keydown', { key: 'Tab' });
    emailInput.dispatchEvent(tabEvent);

    // Can activate button with Space/Enter
    submitButton.focus();
    const spaceEvent = new KeyboardEvent('keydown', { key: ' ' });
    submitButton.dispatchEvent(spaceEvent);
  });

  test('modal traps focus', () => {
    const { rerender } = render(
       {}} title="Test Modal">
        Action 1
        Action 2
      
    );

    const buttons = screen.getAllByRole('button');
    const lastButton = buttons[buttons.length - 1];

    // Focus trap: Tab on last button should go to first
    lastButton.focus();
    const tabEvent = new KeyboardEvent('keydown', {
      key: 'Tab',
      shiftKey: false,
    });
    expect(lastButton === document.activeElement);
  });

  test('alert is announced by screen readers', () => {
    render(
      
    );
    const alert = screen.getByRole('alert');
    expect(alert).toHaveAttribute('aria-live', 'polite');
    expect(alert).toHaveAttribute('aria-atomic', 'true');
  });
});

Quick Reference Tables

Keyboard Events Mapping

| User Action | Key(s) | Component | Purpose | |-------------|--------|-----------|---------| | Navigate | Tab / Shift+Tab | All | Focus previous/next | | Activate | Enter / Space | Button, Checkbox, Radio | Trigger action | | Navigate list | ↑↓ | Menu, Listbox, Select | Move between items | | Navigate horizontal | ← → | Tabs, Slider, Menu | Move between options | | Go to start | Home | Tabs, List, Slider | Jump to first item | | Go to end | End | Tabs, List, Slider | Jump to last item | | Close/Cancel | Escape | Modal, Dropdown, Menu | Dismiss overlay | | Alphabetic | a–z | Listbox, Combobox | Jump to matching item |

ARIA Role Reference (Common)

| Role | Element | Usage | |------|---------|-------| | button | `, (non-button) | Make non-button interactive | | menu / menuitem | , | Popup/context menu | | listbox / option | Custom dropdown | Accessible select replacement | | tab / tablist / tabpanel | Tab UI | Tab container and panels | | dialog | Modal / overlay | Modal dialog (+ aria-modal) | | alert | Notification | Assertive/polite announcement | | region | Generic section | Landmark region (aria-label required) | | navigation` | Nav links | Main/secondary navigation |

Contrast Ratio Quick Check

| Scenario | Min Ratio | Example | |----------|-----------|---------| | Body text (AA) | 4.5:1 | #333 on #fff = 12.6:1 ✓ | | Large text (AA) | 3:1 | #666 on #fff = 5.74:1 ✓ | | Icon/UI (AA) | 3:1 | #0066cc on #fff = 8.59:1 ✓ | | Body text (AAA) | 7:1 | #555 on #fff = 9.26:1 ✓ |


Review Checklist

Use this checklist before marking a component accessible:

##

Source & license

This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.

Install and usage instructions live in the source repository linked above.

Reviews

No reviews yet, be the first.

Versions

  • v0.1.0 Imported from the upstream source.