Back to list
doanchienthangdev

implementing-accessibility

by doanchienthangdev

Omega Vibecode Kit

2🍴 1📅 Jan 21, 2026

SKILL.md


name: implementing-accessibility description: Claude implements WCAG 2.1 AA compliant web interfaces with proper ARIA, keyboard navigation, and screen reader support. Use when building accessible components or auditing existing UIs.

Implementing Accessibility

Quick Start

// Accessible dialog with focus trap and ARIA
export function Dialog({ isOpen, onClose, title, children }: DialogProps) {
  const dialogRef = useRef<HTMLDivElement>(null);
  const titleId = useId();

  useEffect(() => {
    if (isOpen) {
      dialogRef.current?.focus();
      document.body.style.overflow = 'hidden';
    }
    return () => { document.body.style.overflow = ''; };
  }, [isOpen]);

  if (!isOpen) return null;
  return createPortal(
    <>
      <div className="dialog-backdrop" onClick={onClose} />
      <div ref={dialogRef} role="dialog" aria-modal="true" aria-labelledby={titleId} tabIndex={-1}>
        <h2 id={titleId}>{title}</h2>
        {children}
        <button onClick={onClose} aria-label="Close dialog">&times;</button>
      </div>
    </>,
    document.body
  );
}

Features

FeatureDescriptionGuide
Semantic HTMLProper landmarks, headings, and native elementsref/semantic-structure.md
ARIA AttributesRoles, states, and properties for custom widgetsref/aria-patterns.md
Keyboard NavigationTab order, roving tabindex, keyboard shortcutsref/keyboard-nav.md
Focus ManagementFocus trapping, restoration, visible indicatorsref/focus-management.md
Live RegionsDynamic content announcements for screen readersref/live-regions.md
Testingjest-axe, Cypress accessibility, manual testingref/a11y-testing.md

Common Patterns

Accessible Form Field

export function FormField({ id, label, error, hint, required, ...props }: FormFieldProps) {
  const hintId = hint ? `${id}-hint` : undefined;
  const errorId = error ? `${id}-error` : undefined;
  const describedBy = [hintId, errorId].filter(Boolean).join(' ') || undefined;

  return (
    <div className={`form-field ${error ? 'has-error' : ''}`}>
      <label htmlFor={id}>
        {label}
        {required && <span aria-hidden="true">*</span>}
        {required && <span className="sr-only">(required)</span>}
      </label>
      {hint && <p id={hintId} className="form-hint">{hint}</p>}
      <input
        id={id}
        aria-required={required}
        aria-invalid={!!error}
        aria-describedby={describedBy}
        {...props}
      />
      {error && <p id={errorId} role="alert">{error}</p>}
    </div>
  );
}

Roving Tabindex for Tab Component

export function Tabs({ tabs }: { tabs: Tab[] }) {
  const [activeTab, setActiveTab] = useState(0);
  const tabRefs = useRef<HTMLButtonElement[]>([]);

  const handleKeyDown = (e: KeyboardEvent, index: number) => {
    let newIndex = index;
    if (e.key === 'ArrowRight') newIndex = (index + 1) % tabs.length;
    if (e.key === 'ArrowLeft') newIndex = (index - 1 + tabs.length) % tabs.length;
    if (e.key === 'Home') newIndex = 0;
    if (e.key === 'End') newIndex = tabs.length - 1;
    if (newIndex !== index) {
      e.preventDefault();
      setActiveTab(newIndex);
      tabRefs.current[newIndex]?.focus();
    }
  };

  return (
    <div>
      <div role="tablist">
        {tabs.map((tab, i) => (
          <button
            key={tab.id}
            ref={el => tabRefs.current[i] = el!}
            role="tab"
            aria-selected={activeTab === i}
            aria-controls={`panel-${tab.id}`}
            tabIndex={activeTab === i ? 0 : -1}
            onClick={() => setActiveTab(i)}
            onKeyDown={e => handleKeyDown(e, i)}
          >{tab.label}</button>
        ))}
      </div>
      {tabs.map((tab, i) => (
        <div key={tab.id} role="tabpanel" id={`panel-${tab.id}`} hidden={activeTab !== i} tabIndex={0}>
          {tab.content}
        </div>
      ))}
    </div>
  );
}

Live Region Announcer

export function useAnnounce() {
  const [message, setMessage] = useState('');

  const announce = useCallback((text: string) => {
    setMessage('');
    requestAnimationFrame(() => setMessage(text));
    setTimeout(() => setMessage(''), 1000);
  }, []);

  const Announcer = () => (
    <div role="status" aria-live="polite" aria-atomic="true" className="sr-only">
      {message}
    </div>
  );

  return { announce, Announcer };
}

// Usage: announce('3 results found');

Best Practices

DoAvoid
Use semantic HTML elements (<button>, <nav>, <main>)Removing focus outlines without replacement
Provide text alternatives for imagesRelying on color alone to convey information
Ensure 4.5:1 color contrast for textUsing placeholder as the only label
Associate labels with form controlsTrapping keyboard focus unintentionally
Provide skip links for keyboard usersAuto-playing media with sound
Test with actual screen readers (NVDA, VoiceOver)Using ARIA when native HTML suffices
Announce dynamic content changes with live regionsVery small touch targets (min 44x44px)

Score

Total Score

60/100

Based on repository quality metrics

SKILL.md

SKILL.mdファイルが含まれている

+20
LICENSE

ライセンスが設定されている

+10
説明文

100文字以上の説明がある

0/10
人気

GitHub Stars 100以上

0/15
最近の活動

3ヶ月以内に更新がある

0/10
フォーク

10回以上フォークされている

0/5
Issue管理

オープンIssueが50未満

+5
言語

プログラミング言語が設定されている

+5
タグ

1つ以上のタグが設定されている

0/5

Reviews

💬

Reviews coming soon