算法艺术创作
algorithmic-art
通过代码和可调参数创作生成艺术与交互式视觉效果。
frontend-accessibility【设计与创意】Use this skill when the user says 'accessibility', 'a11y', 'WCAG', 'screen reader', 'ARIA', 'keyboard navigation', 'color contrast', 'focus management', 'semantic HTML', or when ensuring frontend applications are accessible. This skill enforces: semantic HTML first (no div soup), ARIA only when native semantics are insufficient, WCAG 2.2 AA complianc…
独立 GitHub 快照 · 自动分类 · 以原作者文档为准
Meet WCAG 2.2 AA standards. Semantic HTML first. ARIA only when necessary. Every interactive element is keyboard accessible. Color contrast meets 4.5:1 minimum. Dynamic content changes announced via live regions. Automated testing enforced in CI.
Exact user phrases: "accessibility", "a11y", "WCAG", "screen reader", "ARIA", "keyboard navigation", "color contrast", "focus management", "semantic HTML", "accessible".
Before activating, verify:
No file output. Produces accessibility review or implementation guidance as text.
Issue:
Element: {selector}
Issue: {description}
WCAG: {success criterion}
Severity: {critical/major/minor}
Fix: {specific code change}
Implementation:
Pattern: {name}
HTML: {semantic element}
ARIA: {aria attributes if needed}
Keyboard: {focus behavior}
No preamble. No postamble. No explanations. No filler/hedging/transitions. Compress output.
Per issue: 6 lines. Per pattern: 8 lines.
Is it interactive?
|-- YES --> Does it navigate?
| |-- YES --> <a href="..."> (even for client-side routing)
| |-- NO --> Does it submit a form?
| |-- YES --> <button type="submit">
| |-- NO --> <button type="button">
|-- NO --> Does it represent a document structure?
|-- YES --> See: <header>, <nav>, <main>, <section>, <article>, <aside>, <footer>
|-- NO --> Is it a group of related items?
|-- YES --> <ul>/<ol> + <li>, or <select>/<option>
|-- NO --> Is it a standalone piece of content?
|-- YES --> <p>, <figure>, <blockquote>, <pre>, <code>
|-- NO --> <div> or <span> as last resort
Does native HTML element exist for this pattern?
|-- YES --> Use native element. Do NOT add ARIA role (overrides native semantics).
|-- NO --> Can the pattern be built with composition of native elements?
|-- YES --> Use composition. No ARIA needed.
|-- NO --> Use ARIA role. Must manage keyboard interaction manually.
|-- role="button", role="tab", role="dialog", role="alertdialog"
|-- role="listbox", role="combobox", role="menu", role="menubar"
|-- role="tree", role="grid", role="slider"
Does the component manage focus?
|-- Modal/Dialog: Trap focus, return to trigger on close
|-- Tab panel: Arrow keys between tabs, roving tabindex
|-- Menu: Arrow keys between items, Escape to close, return focus to trigger
|-- Combobox: Focus stays in input, listbox is programmatically associated
|-- Accordion: Section toggle via Enter/Space, no focus movement
|-- Carousel: Focus wraps, tab stops on carousel shell (not all slides)
Does the component have a focus indicator?
|-- YES --> Is focus indicator at least 2px thick? (WCAG 2.2 2.4.13 Focus Appearance)
| |-- YES --> Meets WCAG 2.2 AA
| |-- NO --> Increase focus indicator to 2px minimum, ensure 3:1 contrast against adjacent
|-- NO --> Add focus indicator (WCAG 2.4.11, 2.4.12, 2.4.13)
Does the user need to dismiss a modal or overlay?
|-- YES --> Add [Dismiss] button, Escape key handler (WCAG 2.2 2.4.11 Focus Not Obscured)
|-- NO --> No action needed
Is there a draggable operation?
|-- YES --> Provide single-pointer alternative (click to move) (WCAG 2.2 2.5.7 Dragging Movements)
|-- NO --> No action needed
Does the component have a target size?
|-- YES --> Is it 24x24px minimum? (WCAG 2.2 2.5.8 Target Size)
|-- YES --> Meets WCAG 2.2 AA
|-- NO --> Increase to 24x24px minimum (exceptions: inline, essential, legal)
WCAG 2.2 adds 9 new success criteria. The most impactful for frontend development:
<!-- NOT THIS -->
<div class="nav">
<div class="nav-item" onclick="navigate()">Home</div>
</div>
<div class="main">
<div class="heading">Welcome</div>
</div>
<!-- THIS -->
<nav aria-label="Main">
<a href="/">Home</a>
</nav>
<main>
<h1>Welcome</h1>
</main>
<button>.| Pattern | Native | ARIA Fallback |
|---|---|---|
| Button | <button> | role="button" tabindex="0" onkeydown |
| Navigation | <nav> | role="navigation" |
| Dialog | <dialog> | role="dialog" aria-modal="true" |
| Progress | <progress> | role="progressbar" aria-valuenow |
| Alert | role="alert" (on any element) | same |
Every interactive element must be keyboard accessible:
<!-- Skip link: first focusable element -->
<a href="#main-content" class="skip-link">Skip to main content</a>
<!-- Focus trap in modals -->
<!-- Tab cycles between first and last focusable element -->
<!-- Escape closes the modal -->
<!-- Focus returns to trigger element on close -->
// React hook for focus trap
function useFocusTrap(modalRef: RefObject<HTMLDivElement>, triggerRef: RefObject<HTMLElement>) {
useEffect(() => {
const modal = modalRef.current
if (!modal) return
const focusable = modal.querySelectorAll<HTMLElement>(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
)
const first = focusable[0]
const last = focusable[focusable.length - 1]
function handleKeyDown(e: KeyboardEvent) {
if (e.key === 'Escape') {
// Close modal, return focus to trigger
modal.dispatchEvent(new CustomEvent('close-modal'))
triggerRef.current?.focus()
return
}
if (e.key === 'Tab') {
if (e.shiftKey && document.activeElement === first) {
e.preventDefault()
last?.focus()
} else if (!e.shiftKey && document.activeElement === last) {
e.preventDefault()
first?.focus()
}
}
}
modal.addEventListener('keydown', handleKeyDown)
first?.focus()
return () => modal.removeEventListener('keydown', handleKeyDown)
}, [modalRef, triggerRef])
}
| Text Type | Minimum Ratio | WCAG Level |
|---|---|---|
| Normal text (<18px / <14px bold) | 4.5:1 | AA |
| Large text (>=18px bold / >=24px) | 3:1 | AA |
| UI components / graphical objects | 3:1 | AA |
| Normal text (AAA) | 7:1 | AAA |
| Large text (AAA) | 4.5:1 | AAA |
| Focus indicator against adjacent | 3:1 | AA (2.2) |
Test with: axe-core, pa11y, Chrome DevTools contrast checker, Colour Contrast Analyser.
// Programmatic contrast check
function meetsContrastRatio(foreground: string, background: string, requiredRatio: number): boolean {
const getLuminance = (hex: string): number => {
const rgb = hex.match(/^#([a-f0-9]{2})([a-f0-9]{2})([a-f0-9]{2})$/i)
if (!rgb) return 0
const [r, g, b] = [rgb[1], rgb[2], rgb[3]].map(c => {
const s = parseInt(c, 16) / 255
return s <= 0.03928 ? s / 12.92 : Math.pow((s + 0.055) / 1.055, 2.4)
})
return 0.2126 * r + 0.7152 * g + 0.0722 * b
}
const L1 = getLuminance(foreground)
const L2 = getLuminance(background)
const ratio = (Math.max(L1, L2) + 0.05) / (Math.min(L1, L2) + 0.05)
return ratio >= requiredRatio
}
/* NEVER */
*:focus { outline: none; }
/* ALWAYS — WCAG 2.2 Focus Appearance */
:focus-visible {
outline: 2px solid var(--color-brand);
outline-offset: 2px;
}
/* For custom focus indicators that meet WCAG 2.2 2.4.13 */
.custom-focus:focus-visible {
outline: 3px solid var(--color-focus-ring);
outline-offset: 3px;
border-radius: 4px;
}
<div aria-live="polite" aria-atomic="true">
{notification message}
</div>
// React pattern for live region announcements
function useAnnouncement() {
const [message, setMessage] = useState('')
const [counter, setCounter] = useState(0)
const announce = useCallback((msg: string) => {
setMessage(msg)
setCounter(c => c + 1) // force re-announce even if same text
}, [])
return {
announce,
LiveRegion: () => (
<div
role="status"
aria-live="polite"
aria-atomic="true"
className="sr-only"
key={counter}
>
{message}
</div>
),
}
}
import { axe, toHaveNoViolations } from 'jest-axe'
expect.extend(toHaveNoViolations)
it('should have no a11y violations', async () => {
const { container } = render(<Button>Click me</Button>)
const results = await axe(container)
expect(results).toHaveNoViolations()
})
// Playwright accessibility test
import { injectAxe, checkA11y } from 'axe-playwright'
test('homepage has no accessibility violations', async ({ page }) => {
await page.goto('/')
await injectAxe(page)
const results = await checkA11y(page, undefined, {
includedImpacts: ['critical', 'serious'],
})
expect(results.violations.length).toBe(0)
})
# GitHub Actions
- name: Accessibility audit
run: |
npx @axe-core/cli http://localhost:3000 \
--exit \
--stdout \
--rules color-contrast,aria-roles,label,landmark-one-main,page-has-heading-one
# Lighthouse CI for a11y budget
- name: Lighthouse CI
run: |
npx lhci collect --url=http://localhost:3000
npx lhci assert --preset=lighthouse:recommended
env:
LHCI_GITHUB_TOKEN: ${{ secrets.LHCI_TOKEN }}
function Dialog({ isOpen, onClose, title, children }: DialogProps) {
const dialogRef = useRef<HTMLDivElement>(null)
const triggerRef = useRef<HTMLElement>(null)
// Focus trap
useFocusTrap(dialogRef, triggerRef)
// Prevent background scroll
useEffect(() => {
if (isOpen) {
document.body.style.overflow = 'hidden'
return () => { document.body.style.overflow = '' }
}
}, [isOpen])
if (!isOpen) return null
return (
<div
role="dialog"
aria-modal="true"
aria-labelledby="dialog-title"
aria-describedby="dialog-description"
ref={dialogRef}
>
<h2 id="dialog-title">{title}</h2>
<div id="dialog-description">{children}</div>
<button onClick={onClose} aria-label="Close dialog">×</button>
</div>
)
}
function Tabs({ tabs }: { tabs: { id: string; label: string; content: ReactNode }[] }) {
const [activeTab, setActiveTab] = useState(0)
const tabListRef = useRef<HTMLDivElement>(null)
const handleKeyDown = (e: React.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)
tabListRef.current?.querySelectorAll<HTMLButtonElement>('[role="tab"]')[newIndex]?.focus()
}
}
return (
<div>
<div role="tablist" aria-label="Content tabs" ref={tabListRef}>
{tabs.map((tab, i) => (
<button
key={tab.id}
role="tab"
id={`tab-${tab.id}`}
aria-selected={i === activeTab}
aria-controls={`panel-${tab.id}`}
tabIndex={i === activeTab ? 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}`}
aria-labelledby={`tab-${tab.id}`}
hidden={i !== activeTab}
>
{tab.content}
</div>
))}
</div>
)
}
function Accordion({ items }: { items: { id: string; summary: string; content: ReactNode }[] }) {
const [openId, setOpenId] = useState<string | null>(null)
return (
<div>
{items.map(item => {
const isOpen = openId === item.id
const buttonId = `accordion-btn-${item.id}`
const panelId = `accordion-panel-${item.id}`
return (
<div key={item.id}>
<h3>
<button
id={buttonId}
aria-expanded={isOpen}
aria-controls={panelId}
onClick={() => setOpenId(isOpen ? null : item.id)}
>
{item.summary}
</button>
</h3>
<div
id={panelId}
role="region"
aria-labelledby={buttonId}
hidden={!isOpen}
>
{item.content}
</div>
</div>
)
})}
</div>
)
}
/* BAD - removes focus indicator entirely */
*:focus { outline: none; }
/* BAD - insufficient contrast */
*:focus { outline: 1px solid #ddd; outline-offset: 0; }
Using color alone to convey state (red = error, green = success) fails WCAG 1.4.1. Always include text labels, icons, or patterns alongside color indicators.
Every form input needs a programmatically associated label.
<!-- WRONG -->
<input type="text" placeholder="Search">
<!-- RIGHT -->
<label for="search">Search</label>
<input type="text" id="search">
<!-- ALSO RIGHT (aria-label for icon-only inputs) -->
<input type="search" aria-label="Search site">
Every custom interactive element must support: Tab (reach), Enter/Space (activate), Escape (dismiss), Arrow keys (navigation within component).
Single Page Applications frequently add/update content without notifying screen readers. Every content update that is visible must also be announced via aria-live or role="status".
Headings must form a logical outline. Skipping from h1 to h4 is invalid. There must be no gaps in the hierarchy (h1 -> h2 -> h3 -> ...).
Every page must have a skip link as the first focusable element. Without it, keyboard users tab through navigation/menus on every page load.
Never put aria-hidden="true" on a focusable element. The element becomes invisible to screen readers but remains focusable, creating a trap where focus disappears.
Sticky headers and persistent footers can hide focused elements. Use scroll-margin-top to ensure focused elements are visible below fixed headers:
:focus-visible {
scroll-margin-top: 80px; /* height of sticky header */
}
Any drag-and-drop interaction must have a click-button alternative:
// Drag alternative: click to select, click to place
function useDragWithAlternative() {
const [mode, setMode] = useState<'drag' | 'click'>('drag')
const [selected, setSelected] = useState<string | null>(null)
return {
mode,
toggleMode: () => setMode(m => m === 'drag' ? 'click' : 'drag'),
selected,
select: setSelected,
...(mode === 'click' ? {
onClick: (id: string) => {
if (!selected) setSelected(id)
else { /* place item */; setSelected(null) }
}
} : {
onDrag: (id: string) => { /* drag logic */ }
})
}
}
| Testing Approach | Automation Level | Coverage | Best For |
|---|---|---|---|
| axe-core | Fully automated | ~57 WCAG rules | CI pipelines, dev feedback |
| Lighthouse a11y | Fully automated | ~23 WCAG rules | Quick audits, performance bundles |
| pa11y | Fully automated | Same as axe-core | CLI-based CI integration |
| WAVE | Semi-automated | Visual overlay | Design review, manual inspection |
| Manual screen reader | Manual | 100% | Final sign-off, edge cases |
| User testing | Manual | Real-world usage | Production validation |
| Accessibility Insights | Semi-automated | FastPass + manual | Enterprise compliance |
@react-aria/live) may add 2-5KB bundle/* Zero-cost focus management */
:focus-visible { outline: 2px solid blue; }
/* vs JS-based */
// ~1KB library needed, event listeners on focus/blur
Prefer CSS :focus-visible over JS-based focus management where possible.
/* Zero-cost fix for WCAG 2.2 Focus Not Obscured */
*:focus {
scroll-margin-top: 80px;
}
/* Each section anchor also needs it */
section[id] {
scroll-margin-top: 80px;
}
name: Accessibility
on: [pull_request]
jobs:
a11y:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npx playwright install --with-deps
- run: npm run build && npm run start & npx wait-on http://localhost:3000
- run: npx @axe-core/cli http://localhost:3000 --exit --chromium
- run: npx lighthouse-ci http://localhost:3000 --score=90
references/a11y-aria.md -- ARIA Patterns & Best Practicesreferences/a11y-testing.md -- Accessibility Testing Patternsreferences/a11y-tools.md -- Accessibility Toolsreferences/accessible-data-visualization.md -- Accessible Data Visualizationreferences/accessible-forms.md -- Accessible Formsreferences/wcag-checklist.md -- WCAG 2.1 AA Checklistreferences/wcag-22-compliance.md -- WCAG 2.2 Compliance Deep Divereferences/accessible-component-patterns.md -- Accessible Component PatternsNo artifact produced. Next skill: frontend-testing -- test accessibility assertions. Carry forward: a11y patterns used, WCAG level, automated testing setup.