opencode-agent-kit 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +796 -0
- package/bin/commands/init.mjs +221 -0
- package/bin/init.mjs +21 -0
- package/package.json +22 -0
- package/template/.opencode/agent-docs/backend/README.md +0 -0
- package/template/.opencode/agent-docs/backend/node/BACKEND_PATTERNS.md +82 -0
- package/template/.opencode/agent-docs/backend/node/BACKEND_QUICK_START.md +49 -0
- package/template/.opencode/agent-docs/frontend/next/README.md +0 -0
- package/template/.opencode/agent-docs/frontend/nuxt/API_PATTERNS.md +807 -0
- package/template/.opencode/agent-docs/frontend/nuxt/CHEATSHEET.md +676 -0
- package/template/.opencode/agent-docs/frontend/nuxt/COMPLETION_REPORT.md +613 -0
- package/template/.opencode/agent-docs/frontend/nuxt/EXAMPLES.md +956 -0
- package/template/.opencode/agent-docs/frontend/nuxt/INDEX.md +596 -0
- package/template/.opencode/agent-docs/frontend/nuxt/MCP_GUIDE.md +881 -0
- package/template/.opencode/agent-docs/frontend/nuxt/MENTOR_CURRICULUM_30_DAYS.md +256 -0
- package/template/.opencode/agent-docs/frontend/nuxt/MENTOR_CURRICULUM_CHECKLIST.md +156 -0
- package/template/.opencode/agent-docs/frontend/nuxt/MENTOR_WEEKLY_ASSIGNMENTS.md +191 -0
- package/template/.opencode/agent-docs/frontend/nuxt/QUICK_START.md +509 -0
- package/template/.opencode/agent-docs/frontend/nuxt/README.md +506 -0
- package/template/.opencode/agent-docs/frontend/nuxt/README_AGENTS.md +140 -0
- package/template/.opencode/agent-docs/frontend/nuxt/README_DOCS.md +65 -0
- package/template/.opencode/agent-docs/frontend/nuxt/SUMMARY.md +474 -0
- package/template/.opencode/agent-docs/frontend/nuxt/TEAM_OPERATING_GUIDE.md +54 -0
- package/template/.opencode/agent-docs/frontend/nuxt/TESTING_GUIDE.md +904 -0
- package/template/.opencode/agent-docs/frontend/nuxt/WORKFLOWS.md +758 -0
- package/template/.opencode/agent-docs/frontend/react/API_PATTERNS.md +187 -0
- package/template/.opencode/agent-docs/frontend/react/CHEATSHEET.md +87 -0
- package/template/.opencode/agent-docs/frontend/react/INDEX.md +45 -0
- package/template/.opencode/agent-docs/frontend/react/QUICK_START.md +43 -0
- package/template/.opencode/agent-docs/frontend/react/README.md +159 -0
- package/template/.opencode/agent-docs/frontend/vue/README.md +0 -0
- package/template/.opencode/agent-docs/mobile/android/README.md +45 -0
- package/template/.opencode/agent-docs/mobile/flutter/README.md +44 -0
- package/template/.opencode/agents/android-developer.md +418 -0
- package/template/.opencode/agents/code-igniter-3-fullstack.md +345 -0
- package/template/.opencode/agents/code-reviewer.md +517 -0
- package/template/.opencode/agents/database-specialist.md +455 -0
- package/template/.opencode/agents/devops-specialist.md +562 -0
- package/template/.opencode/agents/flutter-developer.md +556 -0
- package/template/.opencode/agents/it-leader.md +911 -0
- package/template/.opencode/agents/laravel-advanced.md +691 -0
- package/template/.opencode/agents/node-backend-developer.md +343 -0
- package/template/.opencode/agents/nuxt-frontend-developer-mentor.md +402 -0
- package/template/.opencode/agents/nuxt-frontend-developer.md +1573 -0
- package/template/.opencode/agents/react-frontend-developer.md +1017 -0
- package/template/.opencode/agents/seo-specialist.md +681 -0
- package/template/.opencode/agents/ui-ux-designer.md +783 -0
- package/template/.opencode/commands/android-build/command.md +25 -0
- package/template/.opencode/commands/android-test/command.md +23 -0
- package/template/.opencode/commands/build-fix.md +29 -0
- package/template/.opencode/commands/checkpoint.md +74 -0
- package/template/.opencode/commands/code-review.md +40 -0
- package/template/.opencode/commands/e2e.md +363 -0
- package/template/.opencode/commands/eval.md +120 -0
- package/template/.opencode/commands/evolve.md +193 -0
- package/template/.opencode/commands/flutter-build/command.md +25 -0
- package/template/.opencode/commands/flutter-test/command.md +24 -0
- package/template/.opencode/commands/go-build.md +183 -0
- package/template/.opencode/commands/go-review.md +148 -0
- package/template/.opencode/commands/go-test.md +268 -0
- package/template/.opencode/commands/gpc-release/command.md +30 -0
- package/template/.opencode/commands/instinct-export.md +91 -0
- package/template/.opencode/commands/instinct-import.md +142 -0
- package/template/.opencode/commands/instinct-status.md +86 -0
- package/template/.opencode/commands/learn.md +70 -0
- package/template/.opencode/commands/multi-backend.md +158 -0
- package/template/.opencode/commands/multi-execute.md +310 -0
- package/template/.opencode/commands/multi-frontend.md +158 -0
- package/template/.opencode/commands/multi-plan.md +261 -0
- package/template/.opencode/commands/multi-workflow.md +183 -0
- package/template/.opencode/commands/orchestrate.md +172 -0
- package/template/.opencode/commands/plan.md +113 -0
- package/template/.opencode/commands/pm2.md +271 -0
- package/template/.opencode/commands/python-review.md +297 -0
- package/template/.opencode/commands/refactor-clean.md +28 -0
- package/template/.opencode/commands/sessions.md +305 -0
- package/template/.opencode/commands/setup-pm.md +80 -0
- package/template/.opencode/commands/skill-create.md +174 -0
- package/template/.opencode/commands/tdd.md +326 -0
- package/template/.opencode/commands/test-coverage.md +27 -0
- package/template/.opencode/commands/update-codemaps.md +17 -0
- package/template/.opencode/commands/update-docs.md +31 -0
- package/template/.opencode/commands/verify.md +59 -0
- package/template/.opencode/config.example.json +309 -0
- package/template/.opencode/config.json +341 -0
- package/template/.opencode/contexts/dev.md +20 -0
- package/template/.opencode/contexts/research.md +26 -0
- package/template/.opencode/contexts/review.md +22 -0
- package/template/.opencode/hooks/hooks.json +169 -0
- package/template/.opencode/instructions/INSTRUCTIONS.md +388 -0
- package/template/.opencode/package.json +5 -0
- package/template/.opencode/rules/README.md +82 -0
- package/template/.opencode/rules/android/gradle.md +62 -0
- package/template/.opencode/rules/android/testing.md +27 -0
- package/template/.opencode/rules/common/agents.md +49 -0
- package/template/.opencode/rules/common/coding-style.md +48 -0
- package/template/.opencode/rules/common/git-workflow.md +45 -0
- package/template/.opencode/rules/common/hooks.md +30 -0
- package/template/.opencode/rules/common/patterns.md +31 -0
- package/template/.opencode/rules/common/performance.md +55 -0
- package/template/.opencode/rules/common/security.md +29 -0
- package/template/.opencode/rules/common/testing.md +29 -0
- package/template/.opencode/rules/flutter/state-management.md +57 -0
- package/template/.opencode/rules/flutter/testing.md +42 -0
- package/template/.opencode/rules/golang/coding-style.md +26 -0
- package/template/.opencode/rules/golang/hooks.md +11 -0
- package/template/.opencode/rules/golang/patterns.md +39 -0
- package/template/.opencode/rules/golang/security.md +28 -0
- package/template/.opencode/rules/golang/testing.md +25 -0
- package/template/.opencode/rules/mobile/performance.md +36 -0
- package/template/.opencode/rules/python/coding-style.md +37 -0
- package/template/.opencode/rules/python/hooks.md +14 -0
- package/template/.opencode/rules/python/patterns.md +34 -0
- package/template/.opencode/rules/python/security.md +25 -0
- package/template/.opencode/rules/python/testing.md +33 -0
- package/template/.opencode/rules/typescript/coding-style.md +58 -0
- package/template/.opencode/rules/typescript/hooks.md +15 -0
- package/template/.opencode/rules/typescript/patterns.md +45 -0
- package/template/.opencode/rules/typescript/security.md +21 -0
- package/template/.opencode/rules/typescript/testing.md +11 -0
- package/template/.opencode/skills/api-documentation/SKILL.md +188 -0
- package/template/.opencode/skills/backend-patterns/SKILL.md +587 -0
- package/template/.opencode/skills/building-components/SKILL.md +37 -0
- package/template/.opencode/skills/building-components/references/accessibility.mdx +819 -0
- package/template/.opencode/skills/building-components/references/as-child.mdx +324 -0
- package/template/.opencode/skills/building-components/references/composition.mdx +239 -0
- package/template/.opencode/skills/building-components/references/data-attributes.mdx +413 -0
- package/template/.opencode/skills/building-components/references/definitions.mdx +258 -0
- package/template/.opencode/skills/building-components/references/design-tokens.mdx +57 -0
- package/template/.opencode/skills/building-components/references/docs.mdx +155 -0
- package/template/.opencode/skills/building-components/references/marketplaces.mdx +144 -0
- package/template/.opencode/skills/building-components/references/npm.mdx +166 -0
- package/template/.opencode/skills/building-components/references/polymorphism.mdx +583 -0
- package/template/.opencode/skills/building-components/references/principles.mdx +61 -0
- package/template/.opencode/skills/building-components/references/registry.mdx +169 -0
- package/template/.opencode/skills/building-components/references/state.mdx +99 -0
- package/template/.opencode/skills/building-components/references/styling.mdx +286 -0
- package/template/.opencode/skills/building-components/references/types.mdx +191 -0
- package/template/.opencode/skills/clickhouse-io/SKILL.md +429 -0
- package/template/.opencode/skills/coding-standards/SKILL.md +520 -0
- package/template/.opencode/skills/configure-ecc/SKILL.md +298 -0
- package/template/.opencode/skills/continuous-learning/SKILL.md +110 -0
- package/template/.opencode/skills/continuous-learning/config.json +18 -0
- package/template/.opencode/skills/continuous-learning/evaluate-session.sh +60 -0
- package/template/.opencode/skills/continuous-learning-v2/SKILL.md +284 -0
- package/template/.opencode/skills/continuous-learning-v2/agents/observer.md +137 -0
- package/template/.opencode/skills/continuous-learning-v2/agents/start-observer.sh +134 -0
- package/template/.opencode/skills/continuous-learning-v2/config.json +41 -0
- package/template/.opencode/skills/continuous-learning-v2/hooks/observe.sh +153 -0
- package/template/.opencode/skills/continuous-learning-v2/scripts/instinct-cli.py +489 -0
- package/template/.opencode/skills/continuous-learning-v2/scripts/test_parse_instinct.py +82 -0
- package/template/.opencode/skills/dart-add-unit-test/SKILL.md +122 -0
- package/template/.opencode/skills/dart-build-cli-app/SKILL.md +185 -0
- package/template/.opencode/skills/dart-collect-coverage/SKILL.md +141 -0
- package/template/.opencode/skills/dart-fix-runtime-errors/SKILL.md +166 -0
- package/template/.opencode/skills/dart-generate-test-mocks/SKILL.md +155 -0
- package/template/.opencode/skills/dart-migrate-to-checks-package/SKILL.md +126 -0
- package/template/.opencode/skills/dart-resolve-package-conflicts/SKILL.md +116 -0
- package/template/.opencode/skills/dart-run-static-analysis/SKILL.md +104 -0
- package/template/.opencode/skills/dart-use-pattern-matching/SKILL.md +146 -0
- package/template/.opencode/skills/django-patterns/SKILL.md +733 -0
- package/template/.opencode/skills/django-security/SKILL.md +592 -0
- package/template/.opencode/skills/django-tdd/SKILL.md +728 -0
- package/template/.opencode/skills/django-verification/SKILL.md +460 -0
- package/template/.opencode/skills/eval-harness/SKILL.md +227 -0
- package/template/.opencode/skills/firebase-basics/SKILL.md +103 -0
- package/template/.opencode/skills/firebase-basics/references/additional-skills.md +113 -0
- package/template/.opencode/skills/firebase-basics/references/cli-usage.md +31 -0
- package/template/.opencode/skills/firebase-basics/references/client-library-usage.md +45 -0
- package/template/.opencode/skills/firebase-basics/references/core-concepts.md +61 -0
- package/template/.opencode/skills/firebase-basics/references/iac-usage.md +40 -0
- package/template/.opencode/skills/firebase-basics/references/iam-security.md +74 -0
- package/template/.opencode/skills/firebase-basics/references/mcp-usage.md +63 -0
- package/template/.opencode/skills/flutter/SKILL.md +292 -0
- package/template/.opencode/skills/flutter-add-integration-test/SKILL.md +163 -0
- package/template/.opencode/skills/flutter-add-widget-preview/SKILL.md +145 -0
- package/template/.opencode/skills/flutter-add-widget-test/SKILL.md +154 -0
- package/template/.opencode/skills/flutter-apply-architecture-best-practices/SKILL.md +162 -0
- package/template/.opencode/skills/flutter-build-responsive-layout/SKILL.md +139 -0
- package/template/.opencode/skills/flutter-fix-layout-issues/SKILL.md +130 -0
- package/template/.opencode/skills/flutter-implement-json-serialization/SKILL.md +153 -0
- package/template/.opencode/skills/flutter-setup-declarative-routing/SKILL.md +255 -0
- package/template/.opencode/skills/flutter-setup-localization/SKILL.md +210 -0
- package/template/.opencode/skills/flutter-use-http-package/SKILL.md +174 -0
- package/template/.opencode/skills/frontend-design/SKILL.md +89 -0
- package/template/.opencode/skills/frontend-patterns/SKILL.md +631 -0
- package/template/.opencode/skills/golang-patterns/SKILL.md +673 -0
- package/template/.opencode/skills/golang-testing/SKILL.md +719 -0
- package/template/.opencode/skills/impeccable/SKILL.md +165 -0
- package/template/.opencode/skills/impeccable/agents/impeccable-asset-producer.md +101 -0
- package/template/.opencode/skills/impeccable/reference/adapt.md +190 -0
- package/template/.opencode/skills/impeccable/reference/animate.md +175 -0
- package/template/.opencode/skills/impeccable/reference/audit.md +133 -0
- package/template/.opencode/skills/impeccable/reference/bolder.md +113 -0
- package/template/.opencode/skills/impeccable/reference/brand.md +118 -0
- package/template/.opencode/skills/impeccable/reference/clarify.md +174 -0
- package/template/.opencode/skills/impeccable/reference/codex.md +105 -0
- package/template/.opencode/skills/impeccable/reference/cognitive-load.md +106 -0
- package/template/.opencode/skills/impeccable/reference/color-and-contrast.md +105 -0
- package/template/.opencode/skills/impeccable/reference/colorize.md +154 -0
- package/template/.opencode/skills/impeccable/reference/craft.md +123 -0
- package/template/.opencode/skills/impeccable/reference/critique.md +273 -0
- package/template/.opencode/skills/impeccable/reference/delight.md +302 -0
- package/template/.opencode/skills/impeccable/reference/distill.md +111 -0
- package/template/.opencode/skills/impeccable/reference/document.md +427 -0
- package/template/.opencode/skills/impeccable/reference/extract.md +69 -0
- package/template/.opencode/skills/impeccable/reference/harden.md +347 -0
- package/template/.opencode/skills/impeccable/reference/heuristics-scoring.md +234 -0
- package/template/.opencode/skills/impeccable/reference/interaction-design.md +195 -0
- package/template/.opencode/skills/impeccable/reference/layout.md +141 -0
- package/template/.opencode/skills/impeccable/reference/live.md +622 -0
- package/template/.opencode/skills/impeccable/reference/motion-design.md +109 -0
- package/template/.opencode/skills/impeccable/reference/onboard.md +234 -0
- package/template/.opencode/skills/impeccable/reference/optimize.md +258 -0
- package/template/.opencode/skills/impeccable/reference/overdrive.md +130 -0
- package/template/.opencode/skills/impeccable/reference/personas.md +179 -0
- package/template/.opencode/skills/impeccable/reference/polish.md +242 -0
- package/template/.opencode/skills/impeccable/reference/product.md +62 -0
- package/template/.opencode/skills/impeccable/reference/quieter.md +99 -0
- package/template/.opencode/skills/impeccable/reference/responsive-design.md +114 -0
- package/template/.opencode/skills/impeccable/reference/shape.md +165 -0
- package/template/.opencode/skills/impeccable/reference/spatial-design.md +100 -0
- package/template/.opencode/skills/impeccable/reference/teach.md +156 -0
- package/template/.opencode/skills/impeccable/reference/typeset.md +124 -0
- package/template/.opencode/skills/impeccable/reference/typography.md +159 -0
- package/template/.opencode/skills/impeccable/reference/ux-writing.md +107 -0
- package/template/.opencode/skills/impeccable/scripts/cleanup-deprecated.mjs +284 -0
- package/template/.opencode/skills/impeccable/scripts/command-metadata.json +94 -0
- package/template/.opencode/skills/impeccable/scripts/critique-storage.mjs +242 -0
- package/template/.opencode/skills/impeccable/scripts/design-parser.mjs +820 -0
- package/template/.opencode/skills/impeccable/scripts/detect-csp.mjs +198 -0
- package/template/.opencode/skills/impeccable/scripts/detect.mjs +21 -0
- package/template/.opencode/skills/impeccable/scripts/impeccable-paths.mjs +110 -0
- package/template/.opencode/skills/impeccable/scripts/is-generated.mjs +69 -0
- package/template/.opencode/skills/impeccable/scripts/live-accept.mjs +595 -0
- package/template/.opencode/skills/impeccable/scripts/live-browser-session.js +123 -0
- package/template/.opencode/skills/impeccable/scripts/live-browser.js +4860 -0
- package/template/.opencode/skills/impeccable/scripts/live-complete.mjs +75 -0
- package/template/.opencode/skills/impeccable/scripts/live-completion.mjs +18 -0
- package/template/.opencode/skills/impeccable/scripts/live-inject.mjs +446 -0
- package/template/.opencode/skills/impeccable/scripts/live-poll.mjs +200 -0
- package/template/.opencode/skills/impeccable/scripts/live-resume.mjs +48 -0
- package/template/.opencode/skills/impeccable/scripts/live-server.mjs +838 -0
- package/template/.opencode/skills/impeccable/scripts/live-session-store.mjs +254 -0
- package/template/.opencode/skills/impeccable/scripts/live-status.mjs +47 -0
- package/template/.opencode/skills/impeccable/scripts/live-wrap.mjs +632 -0
- package/template/.opencode/skills/impeccable/scripts/live.mjs +247 -0
- package/template/.opencode/skills/impeccable/scripts/load-context.mjs +141 -0
- package/template/.opencode/skills/impeccable/scripts/modern-screenshot.umd.js +14 -0
- package/template/.opencode/skills/impeccable/scripts/pin.mjs +214 -0
- package/template/.opencode/skills/iterative-retrieval/SKILL.md +202 -0
- package/template/.opencode/skills/java-coding-standards/SKILL.md +138 -0
- package/template/.opencode/skills/jetpack-compose/.skillfish.json +10 -0
- package/template/.opencode/skills/jetpack-compose/SKILL.md +420 -0
- package/template/.opencode/skills/jpa-patterns/SKILL.md +141 -0
- package/template/.opencode/skills/nutrient-document-processing/SKILL.md +165 -0
- package/template/.opencode/skills/nuxt-ui/SKILL.md +334 -0
- package/template/.opencode/skills/nuxt-ui/references/components.md +377 -0
- package/template/.opencode/skills/nuxt-ui/references/composables.md +127 -0
- package/template/.opencode/skills/nuxt-ui/references/layouts/chat.md +266 -0
- package/template/.opencode/skills/nuxt-ui/references/layouts/dashboard.md +220 -0
- package/template/.opencode/skills/nuxt-ui/references/layouts/docs.md +141 -0
- package/template/.opencode/skills/nuxt-ui/references/layouts/editor.md +168 -0
- package/template/.opencode/skills/nuxt-ui/references/layouts/page.md +260 -0
- package/template/.opencode/skills/nuxt-ui/references/theming.md +427 -0
- package/template/.opencode/skills/postgres-patterns/SKILL.md +146 -0
- package/template/.opencode/skills/project-guidelines-example/SKILL.md +345 -0
- package/template/.opencode/skills/python-patterns/SKILL.md +749 -0
- package/template/.opencode/skills/python-testing/SKILL.md +815 -0
- package/template/.opencode/skills/security-review/SKILL.md +494 -0
- package/template/.opencode/skills/security-review/cloud-infrastructure-security.md +361 -0
- package/template/.opencode/skills/shadcn-ui/README.md +248 -0
- package/template/.opencode/skills/shadcn-ui/SKILL.md +326 -0
- package/template/.opencode/skills/shadcn-ui/examples/auth-layout.tsx +177 -0
- package/template/.opencode/skills/shadcn-ui/examples/data-table.tsx +313 -0
- package/template/.opencode/skills/shadcn-ui/examples/form-pattern.tsx +177 -0
- package/template/.opencode/skills/shadcn-ui/resources/component-catalog.md +481 -0
- package/template/.opencode/skills/shadcn-ui/resources/customization-guide.md +516 -0
- package/template/.opencode/skills/shadcn-ui/resources/migration-guide.md +463 -0
- package/template/.opencode/skills/shadcn-ui/resources/setup-guide.md +412 -0
- package/template/.opencode/skills/shadcn-ui/scripts/verify-setup.sh +134 -0
- package/template/.opencode/skills/springboot-patterns/SKILL.md +304 -0
- package/template/.opencode/skills/springboot-security/SKILL.md +119 -0
- package/template/.opencode/skills/springboot-tdd/SKILL.md +157 -0
- package/template/.opencode/skills/springboot-verification/SKILL.md +100 -0
- package/template/.opencode/skills/strategic-compact/SKILL.md +63 -0
- package/template/.opencode/skills/strategic-compact/suggest-compact.sh +52 -0
- package/template/.opencode/skills/tdd-workflow/SKILL.md +409 -0
- package/template/.opencode/skills/vercel-composition-patterns/AGENTS.md +946 -0
- package/template/.opencode/skills/vercel-composition-patterns/SKILL.md +89 -0
- package/template/.opencode/skills/vercel-composition-patterns/rules/architecture-avoid-boolean-props.md +100 -0
- package/template/.opencode/skills/vercel-composition-patterns/rules/architecture-compound-components.md +112 -0
- package/template/.opencode/skills/vercel-composition-patterns/rules/patterns-children-over-render-props.md +87 -0
- package/template/.opencode/skills/vercel-composition-patterns/rules/patterns-explicit-variants.md +100 -0
- package/template/.opencode/skills/vercel-composition-patterns/rules/react19-no-forwardref.md +42 -0
- package/template/.opencode/skills/vercel-composition-patterns/rules/state-context-interface.md +191 -0
- package/template/.opencode/skills/vercel-composition-patterns/rules/state-decouple-implementation.md +113 -0
- package/template/.opencode/skills/vercel-composition-patterns/rules/state-lift-state.md +125 -0
- package/template/.opencode/skills/vercel-react-best-practices/AGENTS.md +2934 -0
- package/template/.opencode/skills/vercel-react-best-practices/SKILL.md +136 -0
- package/template/.opencode/skills/vercel-react-best-practices/rules/advanced-event-handler-refs.md +55 -0
- package/template/.opencode/skills/vercel-react-best-practices/rules/advanced-init-once.md +42 -0
- package/template/.opencode/skills/vercel-react-best-practices/rules/advanced-use-latest.md +39 -0
- package/template/.opencode/skills/vercel-react-best-practices/rules/async-api-routes.md +38 -0
- package/template/.opencode/skills/vercel-react-best-practices/rules/async-defer-await.md +80 -0
- package/template/.opencode/skills/vercel-react-best-practices/rules/async-dependencies.md +51 -0
- package/template/.opencode/skills/vercel-react-best-practices/rules/async-parallel.md +28 -0
- package/template/.opencode/skills/vercel-react-best-practices/rules/async-suspense-boundaries.md +99 -0
- package/template/.opencode/skills/vercel-react-best-practices/rules/bundle-barrel-imports.md +59 -0
- package/template/.opencode/skills/vercel-react-best-practices/rules/bundle-conditional.md +31 -0
- package/template/.opencode/skills/vercel-react-best-practices/rules/bundle-defer-third-party.md +49 -0
- package/template/.opencode/skills/vercel-react-best-practices/rules/bundle-dynamic-imports.md +35 -0
- package/template/.opencode/skills/vercel-react-best-practices/rules/bundle-preload.md +50 -0
- package/template/.opencode/skills/vercel-react-best-practices/rules/client-event-listeners.md +74 -0
- package/template/.opencode/skills/vercel-react-best-practices/rules/client-localstorage-schema.md +71 -0
- package/template/.opencode/skills/vercel-react-best-practices/rules/client-passive-event-listeners.md +48 -0
- package/template/.opencode/skills/vercel-react-best-practices/rules/client-swr-dedup.md +56 -0
- package/template/.opencode/skills/vercel-react-best-practices/rules/js-batch-dom-css.md +107 -0
- package/template/.opencode/skills/vercel-react-best-practices/rules/js-cache-function-results.md +80 -0
- package/template/.opencode/skills/vercel-react-best-practices/rules/js-cache-property-access.md +28 -0
- package/template/.opencode/skills/vercel-react-best-practices/rules/js-cache-storage.md +70 -0
- package/template/.opencode/skills/vercel-react-best-practices/rules/js-combine-iterations.md +32 -0
- package/template/.opencode/skills/vercel-react-best-practices/rules/js-early-exit.md +50 -0
- package/template/.opencode/skills/vercel-react-best-practices/rules/js-hoist-regexp.md +45 -0
- package/template/.opencode/skills/vercel-react-best-practices/rules/js-index-maps.md +37 -0
- package/template/.opencode/skills/vercel-react-best-practices/rules/js-length-check-first.md +49 -0
- package/template/.opencode/skills/vercel-react-best-practices/rules/js-min-max-loop.md +82 -0
- package/template/.opencode/skills/vercel-react-best-practices/rules/js-set-map-lookups.md +24 -0
- package/template/.opencode/skills/vercel-react-best-practices/rules/js-tosorted-immutable.md +57 -0
- package/template/.opencode/skills/vercel-react-best-practices/rules/rendering-activity.md +26 -0
- package/template/.opencode/skills/vercel-react-best-practices/rules/rendering-animate-svg-wrapper.md +47 -0
- package/template/.opencode/skills/vercel-react-best-practices/rules/rendering-conditional-render.md +40 -0
- package/template/.opencode/skills/vercel-react-best-practices/rules/rendering-content-visibility.md +38 -0
- package/template/.opencode/skills/vercel-react-best-practices/rules/rendering-hoist-jsx.md +46 -0
- package/template/.opencode/skills/vercel-react-best-practices/rules/rendering-hydration-no-flicker.md +82 -0
- package/template/.opencode/skills/vercel-react-best-practices/rules/rendering-hydration-suppress-warning.md +30 -0
- package/template/.opencode/skills/vercel-react-best-practices/rules/rendering-svg-precision.md +28 -0
- package/template/.opencode/skills/vercel-react-best-practices/rules/rendering-usetransition-loading.md +75 -0
- package/template/.opencode/skills/vercel-react-best-practices/rules/rerender-defer-reads.md +39 -0
- package/template/.opencode/skills/vercel-react-best-practices/rules/rerender-dependencies.md +45 -0
- package/template/.opencode/skills/vercel-react-best-practices/rules/rerender-derived-state-no-effect.md +40 -0
- package/template/.opencode/skills/vercel-react-best-practices/rules/rerender-derived-state.md +29 -0
- package/template/.opencode/skills/vercel-react-best-practices/rules/rerender-functional-setstate.md +74 -0
- package/template/.opencode/skills/vercel-react-best-practices/rules/rerender-lazy-state-init.md +58 -0
- package/template/.opencode/skills/vercel-react-best-practices/rules/rerender-memo-with-default-value.md +38 -0
- package/template/.opencode/skills/vercel-react-best-practices/rules/rerender-memo.md +44 -0
- package/template/.opencode/skills/vercel-react-best-practices/rules/rerender-move-effect-to-event.md +45 -0
- package/template/.opencode/skills/vercel-react-best-practices/rules/rerender-simple-expression-in-memo.md +35 -0
- package/template/.opencode/skills/vercel-react-best-practices/rules/rerender-transitions.md +40 -0
- package/template/.opencode/skills/vercel-react-best-practices/rules/rerender-use-ref-transient-values.md +73 -0
- package/template/.opencode/skills/vercel-react-best-practices/rules/server-after-nonblocking.md +73 -0
- package/template/.opencode/skills/vercel-react-best-practices/rules/server-auth-actions.md +96 -0
- package/template/.opencode/skills/vercel-react-best-practices/rules/server-cache-lru.md +41 -0
- package/template/.opencode/skills/vercel-react-best-practices/rules/server-cache-react.md +76 -0
- package/template/.opencode/skills/vercel-react-best-practices/rules/server-dedup-props.md +65 -0
- package/template/.opencode/skills/vercel-react-best-practices/rules/server-parallel-fetching.md +83 -0
- package/template/.opencode/skills/vercel-react-best-practices/rules/server-serialization.md +38 -0
- package/template/.opencode/skills/verification-loop/SKILL.md +120 -0
- package/template/.opencode/skills/web-design-guidelines/SKILL.md +39 -0
- package/template/AGENTS.md +32 -0
- package/template/opencode.json +354 -0
|
@@ -0,0 +1,819 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Accessibility
|
|
3
|
+
description: Building components that are usable by everyone, including users with disabilities who rely on assistive technologies.
|
|
4
|
+
type: guide
|
|
5
|
+
summary: Semantic HTML, keyboard navigation, ARIA patterns, focus management, color contrast, and common accessibility pitfalls.
|
|
6
|
+
prerequisites:
|
|
7
|
+
- /definitions
|
|
8
|
+
- /composition
|
|
9
|
+
related:
|
|
10
|
+
- /data-attributes
|
|
11
|
+
- /types
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
Accessibility (a11y) is not an optional feature—it's a fundamental requirement for modern web components. Every component must be usable by everyone, including people with visual, motor, auditory, or cognitive disabilities.
|
|
15
|
+
|
|
16
|
+
This guide is a non-exhaustive list of accessibility principles and patterns that you should follow when building components. It's not a comprehensive guide, but it should give you a sense of the types of issues you should be aware of.
|
|
17
|
+
|
|
18
|
+
If you use a linter with strong accessibility rules like [Ultracite](https://www.ultracite.ai), these types of issues will likely be caught automatically, but it's still important to understand the principles.
|
|
19
|
+
|
|
20
|
+
## Core Principles
|
|
21
|
+
|
|
22
|
+
### 1. Semantic HTML First
|
|
23
|
+
|
|
24
|
+
Always start with the most appropriate HTML element. Semantic HTML provides built-in accessibility features that custom implementations often miss.
|
|
25
|
+
|
|
26
|
+
```tsx
|
|
27
|
+
// ❌ Don't reinvent the wheel
|
|
28
|
+
<div onClick={handleClick} className="button">
|
|
29
|
+
Click me
|
|
30
|
+
</div>
|
|
31
|
+
|
|
32
|
+
// ✅ Use semantic elements
|
|
33
|
+
<button onClick={handleClick}>
|
|
34
|
+
Click me
|
|
35
|
+
</button>
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Semantic elements come with proper role announcements, keyboard interaction, focus management, and form participation.
|
|
39
|
+
|
|
40
|
+
### 2. Keyboard Navigation
|
|
41
|
+
|
|
42
|
+
Every interactive element must be keyboard accessible. Users should be able to navigate, activate, and interact with all functionality using only a keyboard.
|
|
43
|
+
|
|
44
|
+
```tsx
|
|
45
|
+
// ✅ Complete keyboard support
|
|
46
|
+
function Menu() {
|
|
47
|
+
const handleKeyDown = (e: React.KeyboardEvent) => {
|
|
48
|
+
switch (e.key) {
|
|
49
|
+
case "ArrowDown":
|
|
50
|
+
focusNextItem();
|
|
51
|
+
break;
|
|
52
|
+
case "ArrowUp":
|
|
53
|
+
focusPreviousItem();
|
|
54
|
+
break;
|
|
55
|
+
case "Home":
|
|
56
|
+
focusFirstItem();
|
|
57
|
+
break;
|
|
58
|
+
case "End":
|
|
59
|
+
focusLastItem();
|
|
60
|
+
break;
|
|
61
|
+
case "Escape":
|
|
62
|
+
closeMenu();
|
|
63
|
+
break;
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
return (
|
|
68
|
+
<div role="menu" onKeyDown={handleKeyDown}>
|
|
69
|
+
{/* menu items */}
|
|
70
|
+
</div>
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### 3. Screen Reader Support
|
|
76
|
+
|
|
77
|
+
Ensure all content and interactions are announced properly to screen readers using ARIA attributes when necessary.
|
|
78
|
+
|
|
79
|
+
```tsx
|
|
80
|
+
// ✅ Proper ARIA labeling
|
|
81
|
+
<nav aria-label="Main navigation">
|
|
82
|
+
<ul>
|
|
83
|
+
<li><a href="/" aria-current="page">Home</a></li>
|
|
84
|
+
<li><a href="/about">About</a></li>
|
|
85
|
+
</ul>
|
|
86
|
+
</nav>
|
|
87
|
+
|
|
88
|
+
// ✅ Dynamic content announcements
|
|
89
|
+
<div aria-live="polite" aria-atomic="true">
|
|
90
|
+
{isLoading && <span>Loading results...</span>}
|
|
91
|
+
{results && <span>{results.length} results found</span>}
|
|
92
|
+
</div>
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
### 4. Visual Accessibility
|
|
96
|
+
|
|
97
|
+
Support users with visual impairments through proper contrast, focus indicators, and responsive text sizing.
|
|
98
|
+
|
|
99
|
+
```css
|
|
100
|
+
/* ✅ Visible focus indicators */
|
|
101
|
+
button:focus-visible {
|
|
102
|
+
outline: 2px solid var(--color-focus);
|
|
103
|
+
outline-offset: 2px;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/* ✅ Sufficient color contrast (4.5:1 for normal text, 3:1 for large text) */
|
|
107
|
+
.text {
|
|
108
|
+
color: #333; /* Against white: 12.6:1 ratio */
|
|
109
|
+
background: white;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/* ✅ Responsive text sizing */
|
|
113
|
+
.text {
|
|
114
|
+
font-size: 1rem; /* Respects user preferences */
|
|
115
|
+
}
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
## ARIA Patterns
|
|
119
|
+
|
|
120
|
+
### Understanding ARIA
|
|
121
|
+
|
|
122
|
+
ARIA (Accessible Rich Internet Applications) provides semantic information about elements to assistive technologies. Use ARIA to enhance, not replace, semantic HTML.
|
|
123
|
+
|
|
124
|
+
It has a few rules that you should follow:
|
|
125
|
+
|
|
126
|
+
1. Don't use ARIA if you can use semantic HTML
|
|
127
|
+
2. Don't change native semantics unless necessary
|
|
128
|
+
3. All interactive elements must be keyboard accessible
|
|
129
|
+
4. Don't hide focusable elements from assistive technologies
|
|
130
|
+
5. All interactive elements must have accessible names
|
|
131
|
+
|
|
132
|
+
### Common ARIA Attributes
|
|
133
|
+
|
|
134
|
+
#### Roles
|
|
135
|
+
|
|
136
|
+
Define what an element is:
|
|
137
|
+
|
|
138
|
+
```tsx
|
|
139
|
+
// Widget roles
|
|
140
|
+
<div role="button" tabIndex={0} onClick={handleClick}>
|
|
141
|
+
Custom Button
|
|
142
|
+
</div>
|
|
143
|
+
|
|
144
|
+
// Landmark roles
|
|
145
|
+
<div role="navigation" aria-label="Breadcrumb">
|
|
146
|
+
{/* breadcrumb items */}
|
|
147
|
+
</div>
|
|
148
|
+
|
|
149
|
+
// Live region roles
|
|
150
|
+
<div role="alert">
|
|
151
|
+
Error: Invalid email address
|
|
152
|
+
</div>
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
#### States
|
|
156
|
+
|
|
157
|
+
Describe the current state of an element:
|
|
158
|
+
|
|
159
|
+
```tsx
|
|
160
|
+
// Checked state
|
|
161
|
+
<div
|
|
162
|
+
role="checkbox"
|
|
163
|
+
aria-checked={isChecked}
|
|
164
|
+
tabIndex={0}
|
|
165
|
+
>
|
|
166
|
+
Accept terms
|
|
167
|
+
</div>
|
|
168
|
+
|
|
169
|
+
// Expanded state
|
|
170
|
+
<button
|
|
171
|
+
aria-expanded={isOpen}
|
|
172
|
+
aria-controls="panel-1"
|
|
173
|
+
>
|
|
174
|
+
Toggle Panel
|
|
175
|
+
</button>
|
|
176
|
+
<div id="panel-1" hidden={!isOpen}>
|
|
177
|
+
Panel content
|
|
178
|
+
</div>
|
|
179
|
+
|
|
180
|
+
// Selected state
|
|
181
|
+
<li
|
|
182
|
+
role="option"
|
|
183
|
+
aria-selected={isSelected}
|
|
184
|
+
>
|
|
185
|
+
Option 1
|
|
186
|
+
</li>
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
#### Properties
|
|
190
|
+
|
|
191
|
+
Provide additional information:
|
|
192
|
+
|
|
193
|
+
```tsx
|
|
194
|
+
// Labels and descriptions
|
|
195
|
+
<input
|
|
196
|
+
aria-label="Search"
|
|
197
|
+
aria-describedby="search-help"
|
|
198
|
+
type="search"
|
|
199
|
+
/>
|
|
200
|
+
<span id="search-help">Press Enter to search</span>
|
|
201
|
+
|
|
202
|
+
// Relationships
|
|
203
|
+
<button aria-controls="modal-1">Open Modal</button>
|
|
204
|
+
<div id="modal-1" role="dialog">{/* modal content */}</div>
|
|
205
|
+
|
|
206
|
+
// Required and invalid
|
|
207
|
+
<input
|
|
208
|
+
aria-required="true"
|
|
209
|
+
aria-invalid={hasError}
|
|
210
|
+
aria-errormessage="email-error"
|
|
211
|
+
/>
|
|
212
|
+
<span id="email-error">Please enter a valid email</span>
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
## Component Patterns
|
|
216
|
+
|
|
217
|
+
### Modal/Dialog
|
|
218
|
+
|
|
219
|
+
Modals require careful focus management and keyboard trapping:
|
|
220
|
+
|
|
221
|
+
```tsx
|
|
222
|
+
function Modal({ isOpen, onClose, children }) {
|
|
223
|
+
const modalRef = useRef<HTMLDivElement>(null);
|
|
224
|
+
const previousFocus = useRef<HTMLElement | null>(null);
|
|
225
|
+
|
|
226
|
+
useEffect(() => {
|
|
227
|
+
if (isOpen) {
|
|
228
|
+
// Store current focus
|
|
229
|
+
previousFocus.current = document.activeElement as HTMLElement;
|
|
230
|
+
|
|
231
|
+
// Focus first focusable element in modal
|
|
232
|
+
const firstFocusable = modalRef.current?.querySelector<HTMLElement>(
|
|
233
|
+
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])',
|
|
234
|
+
);
|
|
235
|
+
firstFocusable?.focus();
|
|
236
|
+
|
|
237
|
+
// Prevent body scroll
|
|
238
|
+
document.body.style.overflow = "hidden";
|
|
239
|
+
} else {
|
|
240
|
+
// Restore focus
|
|
241
|
+
previousFocus.current?.focus();
|
|
242
|
+
document.body.style.overflow = "";
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
return () => {
|
|
246
|
+
document.body.style.overflow = "";
|
|
247
|
+
};
|
|
248
|
+
}, [isOpen]);
|
|
249
|
+
|
|
250
|
+
const handleKeyDown = (e: React.KeyboardEvent) => {
|
|
251
|
+
if (e.key === "Escape") {
|
|
252
|
+
onClose();
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
if (e.key === "Tab") {
|
|
256
|
+
// Trap focus within modal
|
|
257
|
+
const focusables = modalRef.current?.querySelectorAll<HTMLElement>(
|
|
258
|
+
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])',
|
|
259
|
+
);
|
|
260
|
+
|
|
261
|
+
if (focusables && focusables.length > 0) {
|
|
262
|
+
const firstFocusable = focusables[0];
|
|
263
|
+
const lastFocusable = focusables[focusables.length - 1];
|
|
264
|
+
|
|
265
|
+
if (e.shiftKey && document.activeElement === firstFocusable) {
|
|
266
|
+
e.preventDefault();
|
|
267
|
+
lastFocusable.focus();
|
|
268
|
+
} else if (!e.shiftKey && document.activeElement === lastFocusable) {
|
|
269
|
+
e.preventDefault();
|
|
270
|
+
firstFocusable.focus();
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
};
|
|
275
|
+
|
|
276
|
+
if (!isOpen) return null;
|
|
277
|
+
|
|
278
|
+
return (
|
|
279
|
+
<div
|
|
280
|
+
role="dialog"
|
|
281
|
+
aria-modal="true"
|
|
282
|
+
aria-labelledby="modal-title"
|
|
283
|
+
ref={modalRef}
|
|
284
|
+
onKeyDown={handleKeyDown}
|
|
285
|
+
className="modal"
|
|
286
|
+
>
|
|
287
|
+
<button
|
|
288
|
+
onClick={onClose}
|
|
289
|
+
aria-label="Close dialog"
|
|
290
|
+
className="close-button"
|
|
291
|
+
>
|
|
292
|
+
×
|
|
293
|
+
</button>
|
|
294
|
+
{children}
|
|
295
|
+
</div>
|
|
296
|
+
);
|
|
297
|
+
}
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
### Dropdown Menu
|
|
301
|
+
|
|
302
|
+
Dropdowns need proper ARIA attributes and keyboard navigation:
|
|
303
|
+
|
|
304
|
+
```tsx
|
|
305
|
+
function DropdownMenu({ items }) {
|
|
306
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
307
|
+
const [selectedIndex, setSelectedIndex] = useState(-1);
|
|
308
|
+
const menuRef = useRef<HTMLUListElement>(null);
|
|
309
|
+
|
|
310
|
+
const handleKeyDown = (e: React.KeyboardEvent) => {
|
|
311
|
+
switch (e.key) {
|
|
312
|
+
case "ArrowDown":
|
|
313
|
+
e.preventDefault();
|
|
314
|
+
if (!isOpen) {
|
|
315
|
+
setIsOpen(true);
|
|
316
|
+
setSelectedIndex(0);
|
|
317
|
+
} else {
|
|
318
|
+
setSelectedIndex((prev) => (prev < items.length - 1 ? prev + 1 : 0));
|
|
319
|
+
}
|
|
320
|
+
break;
|
|
321
|
+
|
|
322
|
+
case "ArrowUp":
|
|
323
|
+
e.preventDefault();
|
|
324
|
+
if (isOpen) {
|
|
325
|
+
setSelectedIndex((prev) => (prev > 0 ? prev - 1 : items.length - 1));
|
|
326
|
+
}
|
|
327
|
+
break;
|
|
328
|
+
|
|
329
|
+
case "Enter":
|
|
330
|
+
case " ":
|
|
331
|
+
e.preventDefault();
|
|
332
|
+
if (!isOpen) {
|
|
333
|
+
setIsOpen(true);
|
|
334
|
+
} else if (selectedIndex >= 0) {
|
|
335
|
+
items[selectedIndex].onClick();
|
|
336
|
+
setIsOpen(false);
|
|
337
|
+
}
|
|
338
|
+
break;
|
|
339
|
+
|
|
340
|
+
case "Escape":
|
|
341
|
+
setIsOpen(false);
|
|
342
|
+
setSelectedIndex(-1);
|
|
343
|
+
break;
|
|
344
|
+
}
|
|
345
|
+
};
|
|
346
|
+
|
|
347
|
+
return (
|
|
348
|
+
<div className="dropdown">
|
|
349
|
+
<button
|
|
350
|
+
aria-haspopup="true"
|
|
351
|
+
aria-expanded={isOpen}
|
|
352
|
+
aria-controls="dropdown-menu"
|
|
353
|
+
onKeyDown={handleKeyDown}
|
|
354
|
+
onClick={() => setIsOpen(!isOpen)}
|
|
355
|
+
>
|
|
356
|
+
Menu
|
|
357
|
+
</button>
|
|
358
|
+
|
|
359
|
+
{isOpen && (
|
|
360
|
+
<ul
|
|
361
|
+
id="dropdown-menu"
|
|
362
|
+
role="menu"
|
|
363
|
+
ref={menuRef}
|
|
364
|
+
onKeyDown={handleKeyDown}
|
|
365
|
+
>
|
|
366
|
+
{items.map((item, index) => (
|
|
367
|
+
<li
|
|
368
|
+
key={item.id}
|
|
369
|
+
role="menuitem"
|
|
370
|
+
tabIndex={-1}
|
|
371
|
+
aria-selected={index === selectedIndex}
|
|
372
|
+
onClick={() => {
|
|
373
|
+
item.onClick();
|
|
374
|
+
setIsOpen(false);
|
|
375
|
+
}}
|
|
376
|
+
>
|
|
377
|
+
{item.label}
|
|
378
|
+
</li>
|
|
379
|
+
))}
|
|
380
|
+
</ul>
|
|
381
|
+
)}
|
|
382
|
+
</div>
|
|
383
|
+
);
|
|
384
|
+
}
|
|
385
|
+
```
|
|
386
|
+
|
|
387
|
+
### Tabs
|
|
388
|
+
|
|
389
|
+
Tab interfaces require specific ARIA patterns and keyboard navigation:
|
|
390
|
+
|
|
391
|
+
```tsx
|
|
392
|
+
function Tabs({ tabs, defaultTab = 0 }) {
|
|
393
|
+
const [activeTab, setActiveTab] = useState(defaultTab);
|
|
394
|
+
|
|
395
|
+
const handleKeyDown = (e: React.KeyboardEvent, index: number) => {
|
|
396
|
+
let newIndex = index;
|
|
397
|
+
|
|
398
|
+
switch (e.key) {
|
|
399
|
+
case "ArrowLeft":
|
|
400
|
+
newIndex = index > 0 ? index - 1 : tabs.length - 1;
|
|
401
|
+
break;
|
|
402
|
+
case "ArrowRight":
|
|
403
|
+
newIndex = index < tabs.length - 1 ? index + 1 : 0;
|
|
404
|
+
break;
|
|
405
|
+
case "Home":
|
|
406
|
+
newIndex = 0;
|
|
407
|
+
break;
|
|
408
|
+
case "End":
|
|
409
|
+
newIndex = tabs.length - 1;
|
|
410
|
+
break;
|
|
411
|
+
default:
|
|
412
|
+
return;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
e.preventDefault();
|
|
416
|
+
setActiveTab(newIndex);
|
|
417
|
+
|
|
418
|
+
// Focus the newly selected tab
|
|
419
|
+
const tabElement = document.getElementById(`tab-${newIndex}`);
|
|
420
|
+
tabElement?.focus();
|
|
421
|
+
};
|
|
422
|
+
|
|
423
|
+
return (
|
|
424
|
+
<div className="tabs">
|
|
425
|
+
<div role="tablist" aria-label="Tabs">
|
|
426
|
+
{tabs.map((tab, index) => (
|
|
427
|
+
<button
|
|
428
|
+
key={tab.id}
|
|
429
|
+
id={`tab-${index}`}
|
|
430
|
+
role="tab"
|
|
431
|
+
aria-selected={activeTab === index}
|
|
432
|
+
aria-controls={`panel-${index}`}
|
|
433
|
+
tabIndex={activeTab === index ? 0 : -1}
|
|
434
|
+
onClick={() => setActiveTab(index)}
|
|
435
|
+
onKeyDown={(e) => handleKeyDown(e, index)}
|
|
436
|
+
>
|
|
437
|
+
{tab.label}
|
|
438
|
+
</button>
|
|
439
|
+
))}
|
|
440
|
+
</div>
|
|
441
|
+
|
|
442
|
+
{tabs.map((tab, index) => (
|
|
443
|
+
<div
|
|
444
|
+
key={tab.id}
|
|
445
|
+
id={`panel-${index}`}
|
|
446
|
+
role="tabpanel"
|
|
447
|
+
aria-labelledby={`tab-${index}`}
|
|
448
|
+
hidden={activeTab !== index}
|
|
449
|
+
tabIndex={0}
|
|
450
|
+
>
|
|
451
|
+
{tab.content}
|
|
452
|
+
</div>
|
|
453
|
+
))}
|
|
454
|
+
</div>
|
|
455
|
+
);
|
|
456
|
+
}
|
|
457
|
+
```
|
|
458
|
+
|
|
459
|
+
### Forms
|
|
460
|
+
|
|
461
|
+
Forms need clear labels, error messages, and validation feedback:
|
|
462
|
+
|
|
463
|
+
```tsx
|
|
464
|
+
function AccessibleForm() {
|
|
465
|
+
const [errors, setErrors] = useState({});
|
|
466
|
+
|
|
467
|
+
return (
|
|
468
|
+
<form aria-label="Contact form">
|
|
469
|
+
<div className="form-group">
|
|
470
|
+
<label htmlFor="email">
|
|
471
|
+
Email Address
|
|
472
|
+
<span aria-label="required">*</span>
|
|
473
|
+
</label>
|
|
474
|
+
<input
|
|
475
|
+
id="email"
|
|
476
|
+
type="email"
|
|
477
|
+
aria-required="true"
|
|
478
|
+
aria-invalid={!!errors.email}
|
|
479
|
+
aria-describedby={errors.email ? "email-error" : "email-help"}
|
|
480
|
+
/>
|
|
481
|
+
<span id="email-help" className="help-text">
|
|
482
|
+
We'll never share your email
|
|
483
|
+
</span>
|
|
484
|
+
{errors.email && (
|
|
485
|
+
<span id="email-error" role="alert" className="error">
|
|
486
|
+
{errors.email}
|
|
487
|
+
</span>
|
|
488
|
+
)}
|
|
489
|
+
</div>
|
|
490
|
+
|
|
491
|
+
<fieldset>
|
|
492
|
+
<legend>Notification Preferences</legend>
|
|
493
|
+
<div>
|
|
494
|
+
<input
|
|
495
|
+
id="notify-email"
|
|
496
|
+
type="checkbox"
|
|
497
|
+
name="notifications"
|
|
498
|
+
value="email"
|
|
499
|
+
/>
|
|
500
|
+
<label htmlFor="notify-email">Email notifications</label>
|
|
501
|
+
</div>
|
|
502
|
+
<div>
|
|
503
|
+
<input
|
|
504
|
+
id="notify-sms"
|
|
505
|
+
type="checkbox"
|
|
506
|
+
name="notifications"
|
|
507
|
+
value="sms"
|
|
508
|
+
/>
|
|
509
|
+
<label htmlFor="notify-sms">SMS notifications</label>
|
|
510
|
+
</div>
|
|
511
|
+
</fieldset>
|
|
512
|
+
|
|
513
|
+
<button type="submit">Submit</button>
|
|
514
|
+
</form>
|
|
515
|
+
);
|
|
516
|
+
}
|
|
517
|
+
```
|
|
518
|
+
|
|
519
|
+
## Focus Management
|
|
520
|
+
|
|
521
|
+
### Focus Visible
|
|
522
|
+
|
|
523
|
+
Show focus indicators only for keyboard navigation:
|
|
524
|
+
|
|
525
|
+
```css
|
|
526
|
+
/* Remove default outline */
|
|
527
|
+
*:focus {
|
|
528
|
+
outline: none;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
/* Show outline only for keyboard focus */
|
|
532
|
+
*:focus-visible {
|
|
533
|
+
outline: 2px solid var(--color-focus);
|
|
534
|
+
outline-offset: 2px;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
/* Custom focus styles for specific components */
|
|
538
|
+
.button:focus-visible {
|
|
539
|
+
box-shadow: 0 0 0 3px rgba(66, 153, 225, 0.5);
|
|
540
|
+
}
|
|
541
|
+
```
|
|
542
|
+
|
|
543
|
+
### Focus Trapping
|
|
544
|
+
|
|
545
|
+
Keep focus within a specific region:
|
|
546
|
+
|
|
547
|
+
```tsx
|
|
548
|
+
function useFocusTrap(ref: React.RefObject<HTMLElement>, isActive: boolean) {
|
|
549
|
+
useEffect(() => {
|
|
550
|
+
if (!isActive || !ref.current) return;
|
|
551
|
+
|
|
552
|
+
const element = ref.current;
|
|
553
|
+
const focusableSelector =
|
|
554
|
+
'a[href], button, textarea, input[type="text"], input[type="radio"], input[type="checkbox"], select, [tabindex]:not([tabindex="-1"])';
|
|
555
|
+
|
|
556
|
+
const focusableElements =
|
|
557
|
+
element.querySelectorAll<HTMLElement>(focusableSelector);
|
|
558
|
+
const firstFocusable = focusableElements[0];
|
|
559
|
+
const lastFocusable = focusableElements[focusableElements.length - 1];
|
|
560
|
+
|
|
561
|
+
const handleTabKey = (e: KeyboardEvent) => {
|
|
562
|
+
if (e.key !== "Tab") return;
|
|
563
|
+
|
|
564
|
+
if (e.shiftKey) {
|
|
565
|
+
if (document.activeElement === firstFocusable) {
|
|
566
|
+
e.preventDefault();
|
|
567
|
+
lastFocusable?.focus();
|
|
568
|
+
}
|
|
569
|
+
} else {
|
|
570
|
+
if (document.activeElement === lastFocusable) {
|
|
571
|
+
e.preventDefault();
|
|
572
|
+
firstFocusable?.focus();
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
};
|
|
576
|
+
|
|
577
|
+
element.addEventListener("keydown", handleTabKey);
|
|
578
|
+
firstFocusable?.focus();
|
|
579
|
+
|
|
580
|
+
return () => {
|
|
581
|
+
element.removeEventListener("keydown", handleTabKey);
|
|
582
|
+
};
|
|
583
|
+
}, [ref, isActive]);
|
|
584
|
+
}
|
|
585
|
+
```
|
|
586
|
+
|
|
587
|
+
### Focus Restoration
|
|
588
|
+
|
|
589
|
+
Return focus to the appropriate element after interactions:
|
|
590
|
+
|
|
591
|
+
```tsx
|
|
592
|
+
function useRestoreFocus() {
|
|
593
|
+
const previousFocus = useRef<HTMLElement | null>(null);
|
|
594
|
+
|
|
595
|
+
const saveFocus = () => {
|
|
596
|
+
previousFocus.current = document.activeElement as HTMLElement;
|
|
597
|
+
};
|
|
598
|
+
|
|
599
|
+
const restoreFocus = () => {
|
|
600
|
+
previousFocus.current?.focus();
|
|
601
|
+
};
|
|
602
|
+
|
|
603
|
+
return { saveFocus, restoreFocus };
|
|
604
|
+
}
|
|
605
|
+
```
|
|
606
|
+
|
|
607
|
+
## Live Regions
|
|
608
|
+
|
|
609
|
+
Announce dynamic content changes to screen readers:
|
|
610
|
+
|
|
611
|
+
### Status Messages
|
|
612
|
+
|
|
613
|
+
```tsx
|
|
614
|
+
// Polite announcement (waits for screen reader to finish)
|
|
615
|
+
<div role="status" aria-live="polite">
|
|
616
|
+
{savedMessage && "Settings saved successfully"}
|
|
617
|
+
</div>
|
|
618
|
+
|
|
619
|
+
// Assertive announcement (interrupts screen reader)
|
|
620
|
+
<div role="alert" aria-live="assertive">
|
|
621
|
+
{errorMessage && `Error: ${errorMessage}`}
|
|
622
|
+
</div>
|
|
623
|
+
|
|
624
|
+
// Loading states
|
|
625
|
+
<div aria-live="polite" aria-busy={isLoading}>
|
|
626
|
+
{isLoading ? "Loading..." : `${items.length} items loaded`}
|
|
627
|
+
</div>
|
|
628
|
+
```
|
|
629
|
+
|
|
630
|
+
### Progress Indicators
|
|
631
|
+
|
|
632
|
+
```tsx
|
|
633
|
+
function ProgressBar({ value, max = 100 }) {
|
|
634
|
+
return (
|
|
635
|
+
<div
|
|
636
|
+
role="progressbar"
|
|
637
|
+
aria-valuenow={value}
|
|
638
|
+
aria-valuemin={0}
|
|
639
|
+
aria-valuemax={max}
|
|
640
|
+
aria-label="Upload progress"
|
|
641
|
+
>
|
|
642
|
+
<div
|
|
643
|
+
className="progress-fill"
|
|
644
|
+
style={{ width: `${(value / max) * 100}%` }}
|
|
645
|
+
/>
|
|
646
|
+
<span className="sr-only">
|
|
647
|
+
{Math.round((value / max) * 100)}% complete
|
|
648
|
+
</span>
|
|
649
|
+
</div>
|
|
650
|
+
);
|
|
651
|
+
}
|
|
652
|
+
```
|
|
653
|
+
|
|
654
|
+
## Color and Contrast
|
|
655
|
+
|
|
656
|
+
### Contrast Requirements
|
|
657
|
+
|
|
658
|
+
Follow WCAG guidelines for color contrast:
|
|
659
|
+
|
|
660
|
+
```css
|
|
661
|
+
/* Normal text (< 18pt or < 14pt bold) */
|
|
662
|
+
.text {
|
|
663
|
+
color: #595959; /* 7:1 ratio against white */
|
|
664
|
+
background: white;
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
/* Large text (≥ 18pt or ≥ 14pt bold) */
|
|
668
|
+
.heading {
|
|
669
|
+
color: #767676; /* 4.5:1 ratio against white */
|
|
670
|
+
font-size: 1.5rem;
|
|
671
|
+
font-weight: bold;
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
/* Non-text elements (icons, borders) */
|
|
675
|
+
.icon {
|
|
676
|
+
color: #949494; /* 3:1 ratio against white */
|
|
677
|
+
}
|
|
678
|
+
```
|
|
679
|
+
|
|
680
|
+
### Color Independence
|
|
681
|
+
|
|
682
|
+
Never convey information through color alone:
|
|
683
|
+
|
|
684
|
+
```tsx
|
|
685
|
+
// ❌ Color only
|
|
686
|
+
<span className="text-red-500">Error</span>
|
|
687
|
+
|
|
688
|
+
// ✅ Color with text/icon
|
|
689
|
+
<span className="text-red-500">
|
|
690
|
+
<ErrorIcon aria-hidden="true" />
|
|
691
|
+
<span>Error: Invalid input</span>
|
|
692
|
+
</span>
|
|
693
|
+
|
|
694
|
+
// ✅ Multiple indicators
|
|
695
|
+
<input
|
|
696
|
+
className={hasError ? 'border-red-500' : 'border-gray-300'}
|
|
697
|
+
aria-invalid={hasError}
|
|
698
|
+
aria-describedby={hasError ? 'error-message' : undefined}
|
|
699
|
+
/>
|
|
700
|
+
{hasError && (
|
|
701
|
+
<span id="error-message" className="text-red-500">
|
|
702
|
+
<ErrorIcon aria-hidden="true" /> This field is required
|
|
703
|
+
</span>
|
|
704
|
+
)}
|
|
705
|
+
```
|
|
706
|
+
|
|
707
|
+
## Mobile Accessibility
|
|
708
|
+
|
|
709
|
+
### Touch Targets
|
|
710
|
+
|
|
711
|
+
Ensure touch targets are large enough:
|
|
712
|
+
|
|
713
|
+
```css
|
|
714
|
+
/* Minimum 44x44px for iOS, 48x48dp for Android */
|
|
715
|
+
.button {
|
|
716
|
+
min-height: 44px;
|
|
717
|
+
min-width: 44px;
|
|
718
|
+
padding: 12px 16px;
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
/* Add invisible touch area for small icons */
|
|
722
|
+
.icon-button {
|
|
723
|
+
position: relative;
|
|
724
|
+
padding: 8px;
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
.icon-button::before {
|
|
728
|
+
content: "";
|
|
729
|
+
position: absolute;
|
|
730
|
+
top: -8px;
|
|
731
|
+
right: -8px;
|
|
732
|
+
bottom: -8px;
|
|
733
|
+
left: -8px;
|
|
734
|
+
}
|
|
735
|
+
```
|
|
736
|
+
|
|
737
|
+
### Viewport and Zoom
|
|
738
|
+
|
|
739
|
+
Allow users to zoom:
|
|
740
|
+
|
|
741
|
+
```html
|
|
742
|
+
<!-- ✅ Allows zooming -->
|
|
743
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
744
|
+
|
|
745
|
+
<!-- ❌ Prevents zooming -->
|
|
746
|
+
<meta
|
|
747
|
+
name="viewport"
|
|
748
|
+
content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no"
|
|
749
|
+
/>
|
|
750
|
+
```
|
|
751
|
+
|
|
752
|
+
## Common Pitfalls
|
|
753
|
+
|
|
754
|
+
### Placeholder Text as Labels
|
|
755
|
+
|
|
756
|
+
Don't use placeholders as the only label:
|
|
757
|
+
|
|
758
|
+
```tsx
|
|
759
|
+
// ❌ Placeholder disappears when typing
|
|
760
|
+
<input placeholder="Email address" />
|
|
761
|
+
|
|
762
|
+
// ✅ Persistent label
|
|
763
|
+
<label>
|
|
764
|
+
Email address
|
|
765
|
+
<input type="email" />
|
|
766
|
+
</label>
|
|
767
|
+
|
|
768
|
+
// ✅ Floating label pattern
|
|
769
|
+
<div className="form-field">
|
|
770
|
+
<input id="email" placeholder=" " />
|
|
771
|
+
<label htmlFor="email">Email address</label>
|
|
772
|
+
</div>
|
|
773
|
+
```
|
|
774
|
+
|
|
775
|
+
### Empty Buttons
|
|
776
|
+
|
|
777
|
+
Always provide accessible text for icon buttons:
|
|
778
|
+
|
|
779
|
+
```tsx
|
|
780
|
+
// ❌ No accessible name
|
|
781
|
+
<button onClick={handleDelete}>
|
|
782
|
+
<TrashIcon />
|
|
783
|
+
</button>
|
|
784
|
+
|
|
785
|
+
// ✅ Screen reader text
|
|
786
|
+
<button onClick={handleDelete} aria-label="Delete item">
|
|
787
|
+
<TrashIcon aria-hidden="true" />
|
|
788
|
+
</button>
|
|
789
|
+
|
|
790
|
+
// ✅ Visually hidden text
|
|
791
|
+
<button onClick={handleDelete}>
|
|
792
|
+
<TrashIcon aria-hidden="true" />
|
|
793
|
+
<span className="sr-only">Delete item</span>
|
|
794
|
+
</button>
|
|
795
|
+
```
|
|
796
|
+
|
|
797
|
+
### Disabled Form Elements
|
|
798
|
+
|
|
799
|
+
Disabled elements aren't focusable, which can confuse users:
|
|
800
|
+
|
|
801
|
+
```tsx
|
|
802
|
+
// ❌ User can't understand why button is disabled
|
|
803
|
+
<button disabled={!isValid}>
|
|
804
|
+
Submit
|
|
805
|
+
</button>
|
|
806
|
+
|
|
807
|
+
// ✅ Use aria-disabled and explain
|
|
808
|
+
<button
|
|
809
|
+
aria-disabled={!isValid}
|
|
810
|
+
aria-describedby="submit-help"
|
|
811
|
+
onClick={isValid ? handleSubmit : undefined}
|
|
812
|
+
className={!isValid ? 'opacity-50 cursor-not-allowed' : ''}
|
|
813
|
+
>
|
|
814
|
+
Submit
|
|
815
|
+
</button>
|
|
816
|
+
<span id="submit-help">
|
|
817
|
+
{!isValid && 'Please fill in all required fields'}
|
|
818
|
+
</span>
|
|
819
|
+
```
|