@su-record/vibe 2.8.52 → 2.8.53
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/.env.example +37 -37
- package/CLAUDE.md +169 -169
- package/LICENSE +21 -21
- package/agents/architect-low.md +41 -41
- package/agents/architect-medium.md +59 -59
- package/agents/architect.md +80 -80
- package/agents/build-error-resolver.md +115 -115
- package/agents/compounder.md +261 -261
- package/agents/diagrammer.md +178 -178
- package/agents/docs/api-documenter.md +99 -99
- package/agents/docs/changelog-writer.md +93 -93
- package/agents/e2e-tester.md +294 -294
- package/agents/event/event-comms.md +78 -78
- package/agents/event/event-content.md +68 -68
- package/agents/event/event-image.md +95 -95
- package/agents/event/event-ops.md +84 -84
- package/agents/event/event-scheduler.md +69 -69
- package/agents/event/event-speaker.md +86 -86
- package/agents/explorer-low.md +42 -42
- package/agents/explorer-medium.md +59 -59
- package/agents/explorer.md +48 -48
- package/agents/implementer-low.md +43 -43
- package/agents/implementer-medium.md +52 -52
- package/agents/implementer.md +54 -54
- package/agents/junior-mentor.md +141 -141
- package/agents/planning/requirements-analyst.md +84 -84
- package/agents/planning/ux-advisor.md +83 -83
- package/agents/qa/acceptance-tester.md +86 -86
- package/agents/qa/edge-case-finder.md +93 -93
- package/agents/qa/qa-coordinator.md +131 -131
- package/agents/refactor-cleaner.md +143 -143
- package/agents/research/best-practices-agent.md +199 -199
- package/agents/research/codebase-patterns-agent.md +157 -157
- package/agents/research/framework-docs-agent.md +188 -188
- package/agents/research/security-advisory-agent.md +213 -213
- package/agents/review/architecture-reviewer.md +107 -107
- package/agents/review/complexity-reviewer.md +116 -116
- package/agents/review/data-integrity-reviewer.md +88 -88
- package/agents/review/git-history-reviewer.md +103 -103
- package/agents/review/performance-reviewer.md +86 -86
- package/agents/review/python-reviewer.md +150 -150
- package/agents/review/rails-reviewer.md +139 -139
- package/agents/review/react-reviewer.md +144 -144
- package/agents/review/security-reviewer.md +80 -80
- package/agents/review/simplicity-reviewer.md +140 -140
- package/agents/review/test-coverage-reviewer.md +116 -116
- package/agents/review/typescript-reviewer.md +127 -127
- package/agents/searcher.md +54 -54
- package/agents/simplifier.md +120 -120
- package/agents/tester.md +49 -49
- package/agents/ui/ui-a11y-auditor.md +93 -93
- package/agents/ui/ui-antipattern-detector.md +102 -102
- package/agents/ui/ui-dataviz-advisor.md +69 -69
- package/agents/ui/ui-design-system-gen.md +57 -57
- package/agents/ui/ui-industry-analyzer.md +49 -49
- package/agents/ui/ui-layout-architect.md +65 -65
- package/agents/ui/ui-stack-implementer.md +68 -68
- package/agents/ui/ux-compliance-reviewer.md +81 -81
- package/agents/ui-previewer.md +258 -258
- package/commands/vibe.analyze.md +379 -379
- package/commands/vibe.docs.md +32 -32
- package/commands/vibe.event.md +163 -163
- package/commands/vibe.figma.md +69 -69
- package/commands/vibe.review.md +686 -686
- package/commands/vibe.run.md +2276 -2276
- package/commands/vibe.spec.md +1195 -1195
- package/commands/vibe.spec.review.md +609 -609
- package/commands/vibe.trace.md +259 -259
- package/commands/vibe.utils.md +413 -413
- package/commands/vibe.verify.md +510 -510
- package/dist/cli/collaborator.js +52 -52
- package/dist/cli/commands/config.js +9 -9
- package/dist/cli/commands/evolution.js +12 -12
- package/dist/cli/commands/figma.js +20 -20
- package/dist/cli/commands/info.js +53 -53
- package/dist/cli/commands/init.js +5 -5
- package/dist/cli/commands/remove.js +14 -14
- package/dist/cli/commands/sentinel.js +27 -27
- package/dist/cli/commands/skills.js +5 -5
- package/dist/cli/commands/slack.js +10 -10
- package/dist/cli/commands/stats.js +6 -6
- package/dist/cli/commands/telegram.js +12 -12
- package/dist/cli/detect.js +32 -32
- package/dist/cli/index.js +51 -51
- package/dist/cli/llm/claude-commands.js +16 -16
- package/dist/cli/llm/config.js +18 -18
- package/dist/cli/llm/gemini-commands.js +16 -16
- package/dist/cli/llm/gpt-commands.js +19 -19
- package/dist/cli/llm/help.js +21 -21
- package/dist/cli/postinstall/cursor-agents.js +32 -32
- package/dist/cli/postinstall/cursor-rules.js +83 -83
- package/dist/cli/postinstall/cursor-skills.js +743 -743
- package/dist/cli/setup/Provisioner.js +42 -42
- package/dist/infra/lib/DeepInit.js +24 -24
- package/dist/infra/lib/IterationTracker.js +11 -11
- package/dist/infra/lib/PythonParser.js +108 -108
- package/dist/infra/lib/ReviewRace.js +96 -96
- package/dist/infra/lib/SkillFrontmatter.js +28 -28
- package/dist/infra/lib/SkillQualityGate.js +9 -9
- package/dist/infra/lib/SkillRepository.js +159 -159
- package/dist/infra/lib/TokenBudgetTracker.d.ts +13 -0
- package/dist/infra/lib/TokenBudgetTracker.d.ts.map +1 -1
- package/dist/infra/lib/TokenBudgetTracker.js +44 -3
- package/dist/infra/lib/TokenBudgetTracker.js.map +1 -1
- package/dist/infra/lib/UltraQA.js +99 -99
- package/dist/infra/lib/autonomy/AuditStore.js +41 -41
- package/dist/infra/lib/autonomy/ConfirmationStore.js +30 -30
- package/dist/infra/lib/autonomy/EventOutbox.js +38 -38
- package/dist/infra/lib/autonomy/PolicyEngine.d.ts +3 -3
- package/dist/infra/lib/autonomy/PolicyEngine.js +18 -18
- package/dist/infra/lib/autonomy/SecuritySentinel.js +1 -1
- package/dist/infra/lib/autonomy/SuggestionStore.js +33 -33
- package/dist/infra/lib/embedding/VectorStore.js +22 -22
- package/dist/infra/lib/evolution/AgentAnalyzer.js +10 -10
- package/dist/infra/lib/evolution/DescriptionOptimizer.js +21 -21
- package/dist/infra/lib/evolution/GenerationRegistry.js +36 -36
- package/dist/infra/lib/evolution/InsightStore.js +90 -90
- package/dist/infra/lib/evolution/ParityTester.js +57 -57
- package/dist/infra/lib/evolution/RollbackManager.js +5 -5
- package/dist/infra/lib/evolution/SkillBenchmark.js +23 -23
- package/dist/infra/lib/evolution/SkillEvalRunner.js +50 -50
- package/dist/infra/lib/evolution/SkillGapDetector.js +10 -10
- package/dist/infra/lib/evolution/UsageTracker.js +28 -28
- package/dist/infra/lib/gemini/orchestration.js +5 -5
- package/dist/infra/lib/gpt/orchestration.js +4 -4
- package/dist/infra/lib/memory/KnowledgeGraph.js +4 -4
- package/dist/infra/lib/memory/MemorySearch.js +57 -57
- package/dist/infra/lib/memory/MemoryStorage.js +181 -181
- package/dist/infra/lib/memory/ObservationStore.js +28 -28
- package/dist/infra/lib/memory/ReflectionStore.js +30 -30
- package/dist/infra/lib/memory/SessionRAGRetriever.js +7 -7
- package/dist/infra/lib/memory/SessionRAGStore.js +225 -225
- package/dist/infra/lib/memory/SessionSummarizer.js +9 -9
- package/dist/infra/orchestrator/AgentManager.js +12 -12
- package/dist/infra/orchestrator/AgentRegistry.js +65 -65
- package/dist/infra/orchestrator/MultiLlmResearch.js +8 -8
- package/dist/infra/orchestrator/SwarmOrchestrator.test.js +16 -16
- package/dist/infra/orchestrator/parallelResearch.js +24 -24
- package/dist/tools/convention/analyzeComplexity.test.js +115 -115
- package/dist/tools/convention/validateCodeQuality.test.js +104 -104
- package/dist/tools/memory/createMemoryTimeline.js +10 -10
- package/dist/tools/memory/getMemoryGraph.js +12 -12
- package/dist/tools/memory/getSessionContext.js +9 -9
- package/dist/tools/memory/linkMemories.js +14 -14
- package/dist/tools/memory/listMemories.js +4 -4
- package/dist/tools/memory/recallMemory.js +4 -4
- package/dist/tools/memory/saveMemory.js +4 -4
- package/dist/tools/memory/searchMemoriesAdvanced.js +23 -23
- package/dist/tools/semantic/analyzeDependencyGraph.js +12 -12
- package/dist/tools/semantic/astGrep.test.js +6 -6
- package/dist/tools/spec/prdParser.test.js +171 -171
- package/dist/tools/spec/specGenerator.js +169 -169
- package/dist/tools/spec/traceabilityMatrix.js +64 -64
- package/dist/tools/spec/traceabilityMatrix.test.js +28 -28
- package/hooks/gemini-hooks.json +73 -73
- package/hooks/hooks.json +174 -174
- package/hooks/scripts/__tests__/keyword-detector.test.js +199 -199
- package/hooks/scripts/__tests__/pre-tool-guard.test.js +286 -286
- package/hooks/scripts/__tests__/sentinel-guard.test.js +210 -210
- package/hooks/scripts/auto-commit.js +97 -97
- package/hooks/scripts/auto-format.js +64 -64
- package/hooks/scripts/auto-test.js +81 -81
- package/hooks/scripts/code-check.js +268 -268
- package/hooks/scripts/codex-detect.js +46 -46
- package/hooks/scripts/codex-review-gate.js +80 -80
- package/hooks/scripts/command-log.js +32 -32
- package/hooks/scripts/context-save.js +353 -353
- package/hooks/scripts/evolution-engine.js +91 -91
- package/hooks/scripts/figma-extract.js +635 -477
- package/hooks/scripts/hud-status.js +321 -321
- package/hooks/scripts/keyword-detector.js +214 -214
- package/hooks/scripts/llm-orchestrate.js +572 -572
- package/hooks/scripts/post-edit.js +32 -32
- package/hooks/scripts/pr-test-gate.js +52 -52
- package/hooks/scripts/pre-tool-guard.js +214 -159
- package/hooks/scripts/prompt-dispatcher.js +185 -185
- package/hooks/scripts/sentinel-guard.js +131 -131
- package/hooks/scripts/session-start.js +177 -177
- package/hooks/scripts/skill-injector.js +83 -83
- package/hooks/scripts/stop-notify.js +209 -209
- package/hooks/scripts/utils.js +243 -243
- package/languages/csharp-unity.md +515 -515
- package/languages/gdscript-godot.md +470 -470
- package/languages/ruby-rails.md +489 -489
- package/languages/typescript-angular.md +433 -433
- package/languages/typescript-astro.md +416 -416
- package/languages/typescript-electron.md +406 -406
- package/languages/typescript-nestjs.md +524 -524
- package/languages/typescript-svelte.md +407 -407
- package/languages/typescript-tauri.md +365 -365
- package/package.json +101 -101
- package/skills/agents-md/SKILL.md +121 -121
- package/skills/agents-md/rubrics/what-to-keep.md +49 -49
- package/skills/agents-md/templates/agents-md.md +36 -36
- package/skills/arch-guard/SKILL.md +181 -181
- package/skills/arch-guard/agents/detector.md +48 -48
- package/skills/arch-guard/agents/reporter.md +48 -48
- package/skills/arch-guard/agents/rule-generator.md +49 -49
- package/skills/arch-guard/agents/violation-checker.md +51 -51
- package/skills/arch-guard/frameworks/clean-architecture.md +108 -108
- package/skills/arch-guard/frameworks/solid.md +102 -102
- package/skills/arch-guard/scripts/check-boundaries.js +90 -90
- package/skills/arch-guard/templates/arch-rules.json +47 -47
- package/skills/arch-guard/templates/violation-report.md +53 -53
- package/skills/brand-assets/SKILL.md +147 -147
- package/skills/brand-assets/rubrics/asset-checklist.md +98 -98
- package/skills/brand-assets/templates/brand-guide.md +161 -161
- package/skills/capability-loop/SKILL.md +168 -168
- package/skills/capability-loop/agents/capability-designer.md +61 -61
- package/skills/capability-loop/agents/failure-analyst.md +55 -55
- package/skills/capability-loop/agents/implementer.md +50 -50
- package/skills/capability-loop/agents/tester.md +53 -53
- package/skills/capability-loop/templates/capability-spec.md +118 -118
- package/skills/capability-loop/templates/failure-analysis.md +118 -118
- package/skills/characterization-test/SKILL.md +207 -207
- package/skills/characterization-test/agents/behavior-capturer.md +50 -50
- package/skills/characterization-test/agents/coverage-checker.md +54 -54
- package/skills/characterization-test/agents/reporter.md +50 -50
- package/skills/characterization-test/agents/test-writer.md +49 -49
- package/skills/characterization-test/rubrics/coverage-criteria.md +53 -53
- package/skills/characterization-test/templates/test-template.ts +101 -101
- package/skills/chub-usage/SKILL.md +139 -139
- package/skills/claude-md-guide/SKILL.md +351 -351
- package/skills/claude-md-guide/rubrics/anti-patterns.md +88 -88
- package/skills/claude-md-guide/templates/claude-md.md +54 -54
- package/skills/commerce-patterns/SKILL.md +64 -64
- package/skills/commerce-patterns/rubrics/checkout-flow.md +48 -48
- package/skills/commerce-patterns/templates/product-schema.md +85 -85
- package/skills/commit-push-pr/SKILL.md +77 -77
- package/skills/commit-push-pr/agents/change-analyzer.md +55 -55
- package/skills/commit-push-pr/agents/message-writer.md +50 -50
- package/skills/commit-push-pr/agents/pr-writer.md +58 -58
- package/skills/commit-push-pr/agents/reviewer.md +52 -52
- package/skills/commit-push-pr/rubrics/commit-message.md +73 -73
- package/skills/commit-push-pr/templates/pr-body.md +63 -63
- package/skills/context7-usage/SKILL.md +106 -106
- package/skills/context7-usage/rubrics/when-to-use.md +50 -50
- package/skills/create-prd/SKILL.md +90 -90
- package/skills/create-prd/agents/edge-case-finder.md +48 -48
- package/skills/create-prd/agents/prioritizer.md +60 -60
- package/skills/create-prd/agents/requirements-writer.md +48 -48
- package/skills/create-prd/agents/researcher.md +55 -55
- package/skills/create-prd/agents/reviewer.md +54 -54
- package/skills/create-prd/frameworks/jobs-to-be-done.md +96 -96
- package/skills/create-prd/frameworks/rice-scoring.md +97 -97
- package/skills/create-prd/orchestrator.md +70 -70
- package/skills/create-prd/rubrics/completeness.md +58 -58
- package/skills/create-prd/templates/prd.md +139 -139
- package/skills/design-audit/SKILL.md +152 -152
- package/skills/design-audit/agents/a11y-auditor.md +43 -43
- package/skills/design-audit/agents/performance-auditor.md +46 -46
- package/skills/design-audit/agents/responsive-auditor.md +46 -46
- package/skills/design-audit/agents/scorer.md +47 -47
- package/skills/design-audit/agents/slop-detector.md +47 -47
- package/skills/design-audit/frameworks/core-web-vitals.md +107 -107
- package/skills/design-audit/frameworks/wcag-checklist.md +64 -64
- package/skills/design-audit/orchestrator.md +64 -64
- package/skills/design-audit/rubrics/ai-slop-patterns.md +83 -83
- package/skills/design-audit/rubrics/scoring.md +63 -63
- package/skills/design-audit/templates/report.md +88 -88
- package/skills/design-critique/SKILL.md +139 -139
- package/skills/design-critique/rubrics/ux-heuristics.md +143 -143
- package/skills/design-critique/templates/critique-report.md +86 -86
- package/skills/design-distill/SKILL.md +130 -130
- package/skills/design-distill/templates/design-system.md +132 -132
- package/skills/design-normalize/SKILL.md +133 -133
- package/skills/design-normalize/rubrics/token-naming.md +117 -117
- package/skills/design-normalize/templates/token-audit.md +89 -89
- package/skills/design-polish/SKILL.md +131 -131
- package/skills/design-polish/rubrics/polish-checklist.md +68 -68
- package/skills/design-polish/templates/polish-report.md +64 -64
- package/skills/design-teach/SKILL.md +182 -182
- package/skills/design-teach/rubrics/brand-personality.md +73 -73
- package/skills/design-teach/templates/design-context.json +36 -36
- package/skills/e2e-commerce/SKILL.md +62 -62
- package/skills/e2e-commerce/templates/test-scenarios.md +170 -170
- package/skills/event-comms/SKILL.md +162 -162
- package/skills/event-comms/templates/email-invite.md +99 -99
- package/skills/event-comms/templates/sns-post.md +133 -133
- package/skills/event-ops/SKILL.md +198 -198
- package/skills/event-ops/rubrics/contingency.md +85 -85
- package/skills/event-ops/templates/d-day-checklist.md +65 -65
- package/skills/event-planning/SKILL.md +132 -132
- package/skills/event-planning/rubrics/timeline.md +70 -70
- package/skills/event-planning/templates/event-plan.md +91 -91
- package/skills/exec-plan/SKILL.md +149 -149
- package/skills/exec-plan/agents/decomposer.md +47 -47
- package/skills/exec-plan/agents/dependency-mapper.md +44 -44
- package/skills/exec-plan/agents/estimator.md +43 -43
- package/skills/exec-plan/agents/validator.md +55 -55
- package/skills/exec-plan/orchestrator.md +70 -70
- package/skills/exec-plan/rubrics/complexity-scoring.md +75 -75
- package/skills/exec-plan/templates/plan.md +147 -147
- package/skills/git-worktree/SKILL.md +73 -73
- package/skills/git-worktree/rubrics/when-to-use.md +55 -55
- package/skills/handoff/SKILL.md +110 -110
- package/skills/handoff/agents/context-summarizer.md +51 -51
- package/skills/handoff/agents/document-writer.md +63 -63
- package/skills/handoff/agents/state-collector.md +53 -53
- package/skills/handoff/agents/verifier.md +48 -48
- package/skills/handoff/rubrics/completeness.md +62 -62
- package/skills/handoff/templates/handoff.md +107 -107
- package/skills/parallel-research/SKILL.md +89 -89
- package/skills/parallel-research/agents/best-practices.md +43 -43
- package/skills/parallel-research/agents/codebase-patterns.md +46 -46
- package/skills/parallel-research/agents/framework-docs.md +45 -45
- package/skills/parallel-research/agents/security-advisory.md +46 -46
- package/skills/parallel-research/agents/synthesizer.md +52 -52
- package/skills/parallel-research/experts/best-practices.md +50 -50
- package/skills/parallel-research/experts/codebase-patterns.md +70 -70
- package/skills/parallel-research/experts/framework-docs.md +65 -65
- package/skills/parallel-research/experts/security-advisory.md +69 -69
- package/skills/parallel-research/orchestrator.md +65 -65
- package/skills/parallel-research/templates/synthesis.md +101 -101
- package/skills/prioritization-frameworks/SKILL.md +87 -87
- package/skills/prioritization-frameworks/rubrics/frameworks.md +79 -79
- package/skills/prioritization-frameworks/templates/scoring-matrix.md +69 -69
- package/skills/priority-todos/SKILL.md +64 -64
- package/skills/priority-todos/rubrics/prioritization.md +70 -70
- package/skills/priority-todos/templates/todo-board.md +59 -59
- package/skills/seo-checklist/SKILL.md +58 -58
- package/skills/seo-checklist/frameworks/structured-data.md +153 -153
- package/skills/seo-checklist/rubrics/content-seo.md +42 -42
- package/skills/seo-checklist/rubrics/technical-seo.md +48 -48
- package/skills/techdebt/SKILL.md +124 -124
- package/skills/techdebt/agents/analyzer.md +50 -50
- package/skills/techdebt/agents/fixer.md +41 -41
- package/skills/techdebt/agents/reviewer.md +47 -47
- package/skills/techdebt/agents/scanner.md +44 -44
- package/skills/techdebt/orchestrator.md +70 -70
- package/skills/techdebt/rubrics/severity.md +51 -51
- package/skills/techdebt/scripts/scan.js +90 -90
- package/skills/techdebt/templates/report.md +86 -86
- package/skills/tool-fallback/SKILL.md +104 -104
- package/skills/tool-fallback/rubrics/fallback-chain.md +58 -58
- package/skills/typescript-advanced-types/SKILL.md +67 -67
- package/skills/typescript-advanced-types/rubrics/type-patterns.md +109 -109
- package/skills/ui-ux-pro-max/SKILL.md +236 -236
- package/skills/ui-ux-pro-max/reference/color-and-contrast.md +517 -517
- package/skills/ui-ux-pro-max/reference/interaction-design.md +544 -544
- package/skills/ui-ux-pro-max/reference/motion-design.md +591 -591
- package/skills/ui-ux-pro-max/reference/responsive-design.md +463 -463
- package/skills/ui-ux-pro-max/reference/spatial-design.md +390 -390
- package/skills/ui-ux-pro-max/reference/typography.md +455 -455
- package/skills/ui-ux-pro-max/reference/ux-writing.md +469 -469
- package/skills/ui-ux-pro-max/rubrics/interaction-states.md +83 -83
- package/skills/ui-ux-pro-max/rubrics/responsive-breakpoints.md +99 -99
- package/skills/user-personas/SKILL.md +75 -75
- package/skills/user-personas/rubrics/research-methods.md +56 -56
- package/skills/user-personas/templates/persona.md +89 -89
- package/skills/vercel-react-best-practices/SKILL.md +60 -60
- package/skills/vercel-react-best-practices/rubrics/performance.md +82 -82
- package/skills/vercel-react-best-practices/rubrics/server-components.md +86 -86
- package/skills/vibe.docs/SKILL.md +171 -171
- package/skills/vibe.docs/templates/architecture.md +80 -80
- package/skills/vibe.docs/templates/readme.md +84 -84
- package/skills/vibe.docs/templates/release-notes.md +74 -74
- package/skills/vibe.figma/SKILL.md +215 -982
- package/skills/vibe.figma/rubrics/extraction-checklist.md +51 -51
- package/skills/vibe.figma/templates/component-index.md +126 -126
- package/skills/vibe.figma/templates/figma-handoff.md +100 -100
- package/skills/vibe.figma/templates/remapped-tree.md +277 -277
- package/skills/vibe.figma.convert/SKILL.md +188 -511
- package/skills/vibe.figma.convert/rubrics/conversion-rules.md +129 -113
- package/skills/vibe.figma.convert/templates/component.md +140 -140
- package/skills/vibe.figma.extract/SKILL.md +179 -300
- package/skills/vibe.figma.extract/rubrics/image-rules.md +145 -137
- package/skills/video-production/SKILL.md +52 -52
- package/skills/video-production/rubrics/quality-checklist.md +58 -58
- package/skills/video-production/templates/production-plan.md +104 -104
- package/vibe/config.json +29 -29
- package/vibe/constitution.md +227 -227
- package/vibe/rules/principles/communication-guide.md +98 -98
- package/vibe/rules/principles/development-philosophy.md +52 -52
- package/vibe/rules/principles/quick-start.md +102 -102
- package/vibe/rules/quality/bdd-contract-testing.md +393 -393
- package/vibe/rules/quality/checklist.md +276 -276
- package/vibe/rules/quality/performance.md +236 -236
- package/vibe/rules/quality/testing-strategy.md +440 -440
- package/vibe/rules/standards/anti-patterns.md +541 -541
- package/vibe/rules/standards/code-structure.md +291 -291
- package/vibe/rules/standards/complexity-metrics.md +313 -313
- package/vibe/rules/standards/git-workflow.md +237 -237
- package/vibe/rules/standards/naming-conventions.md +198 -198
- package/vibe/rules/standards/security.md +305 -305
- package/vibe/rules/writing/document-style.md +74 -74
- package/vibe/setup.sh +31 -31
- package/vibe/templates/constitution-template.md +252 -252
- package/vibe/templates/contract-backend-template.md +526 -526
- package/vibe/templates/contract-frontend-template.md +599 -599
- package/vibe/templates/feature-template.md +96 -96
- package/vibe/templates/spec-template.md +221 -221
- package/vibe/ui-ux-data/charts.csv +26 -26
- package/vibe/ui-ux-data/colors.csv +97 -97
- package/vibe/ui-ux-data/icons.csv +101 -101
- package/vibe/ui-ux-data/landing.csv +31 -31
- package/vibe/ui-ux-data/products.csv +96 -96
- package/vibe/ui-ux-data/react-performance.csv +45 -45
- package/vibe/ui-ux-data/stacks/astro.csv +54 -54
- package/vibe/ui-ux-data/stacks/flutter.csv +53 -53
- package/vibe/ui-ux-data/stacks/html-tailwind.csv +56 -56
- package/vibe/ui-ux-data/stacks/jetpack-compose.csv +53 -53
- package/vibe/ui-ux-data/stacks/nextjs.csv +53 -53
- package/vibe/ui-ux-data/stacks/nuxt-ui.csv +51 -51
- package/vibe/ui-ux-data/stacks/nuxtjs.csv +59 -59
- package/vibe/ui-ux-data/stacks/react-native.csv +52 -52
- package/vibe/ui-ux-data/stacks/react.csv +54 -54
- package/vibe/ui-ux-data/stacks/shadcn.csv +61 -61
- package/vibe/ui-ux-data/stacks/svelte.csv +54 -54
- package/vibe/ui-ux-data/stacks/swiftui.csv +51 -51
- package/vibe/ui-ux-data/stacks/vue.csv +50 -50
- package/vibe/ui-ux-data/styles.csv +68 -68
- package/vibe/ui-ux-data/typography.csv +57 -57
- package/vibe/ui-ux-data/ui-reasoning.csv +101 -101
- package/vibe/ui-ux-data/ux-guidelines.csv +99 -99
- package/vibe/ui-ux-data/version.json +31 -31
- package/vibe/ui-ux-data/web-interface.csv +31 -31
|
@@ -1,544 +1,544 @@
|
|
|
1
|
-
# Interaction Design Reference
|
|
2
|
-
|
|
3
|
-
Deep reference for building interactive UI that is accessible, predictable, and polished. Covers the full lifecycle of a component's states, focus handling, form behavior, overlays, menus, touch targets, and loading feedback.
|
|
4
|
-
|
|
5
|
-
---
|
|
6
|
-
|
|
7
|
-
## 8 Interactive States
|
|
8
|
-
|
|
9
|
-
Every interactive element must account for eight distinct states. Missing even one breaks accessibility or trust.
|
|
10
|
-
|
|
11
|
-
### State Definitions and Implementation
|
|
12
|
-
|
|
13
|
-
**Default** — the element at rest. Provide enough visual affordance that the user knows it is interactive. Borders, shadows, and color contrast carry this signal.
|
|
14
|
-
|
|
15
|
-
**Hover** — pointer is over the element but not pressed. Communicate interactivity with a cursor change and a subtle visual shift. Never rely on hover alone to reveal essential UI.
|
|
16
|
-
|
|
17
|
-
**Focus** — keyboard or programmatic focus. Must be visible at all times for keyboard users. Use `:focus-visible` so mouse users are not shown an outline on click.
|
|
18
|
-
|
|
19
|
-
**Active** — element is being pressed (mousedown / touchstart). Provide immediate feedback: a slight scale-down or darker background confirms the press registered.
|
|
20
|
-
|
|
21
|
-
**Disabled** — element exists but cannot be interacted with. Reduce opacity, change cursor to `not-allowed`, and remove from tab order with `disabled` attribute (not just `aria-disabled`).
|
|
22
|
-
|
|
23
|
-
**Loading** — action has been triggered and the system is working. Replace interactive content with a spinner or skeleton. Disable the trigger to prevent double-submission.
|
|
24
|
-
|
|
25
|
-
**Error** — the action failed or input is invalid. Show a message in red near the source. Use `aria-describedby` to associate the message with the input.
|
|
26
|
-
|
|
27
|
-
**Success** — the action completed. Provide a brief confirmation (green checkmark, toast, inline message). Auto-dismiss non-critical confirmations after 3–5 seconds.
|
|
28
|
-
|
|
29
|
-
```tsx
|
|
30
|
-
// Button covering all 8 states
|
|
31
|
-
type ButtonState = 'default' | 'loading' | 'error' | 'success';
|
|
32
|
-
|
|
33
|
-
interface ButtonProps {
|
|
34
|
-
label: string;
|
|
35
|
-
state?: ButtonState;
|
|
36
|
-
disabled?: boolean;
|
|
37
|
-
onClick: () => void;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
export function Button({ label, state = 'default', disabled = false, onClick }: ButtonProps) {
|
|
41
|
-
const isLoading = state === 'loading';
|
|
42
|
-
const isDisabled = disabled || isLoading;
|
|
43
|
-
|
|
44
|
-
return (
|
|
45
|
-
<button
|
|
46
|
-
onClick={onClick}
|
|
47
|
-
disabled={isDisabled}
|
|
48
|
-
aria-busy={isLoading}
|
|
49
|
-
className={[
|
|
50
|
-
'btn',
|
|
51
|
-
`btn--${state}`,
|
|
52
|
-
isDisabled ? 'btn--disabled' : '',
|
|
53
|
-
].join(' ')}
|
|
54
|
-
>
|
|
55
|
-
{isLoading ? <Spinner size={16} /> : label}
|
|
56
|
-
</button>
|
|
57
|
-
);
|
|
58
|
-
}
|
|
59
|
-
```
|
|
60
|
-
|
|
61
|
-
```css
|
|
62
|
-
.btn {
|
|
63
|
-
cursor: pointer;
|
|
64
|
-
transition: background 120ms ease, transform 80ms ease, opacity 120ms ease;
|
|
65
|
-
}
|
|
66
|
-
.btn:hover:not(:disabled) { background: var(--color-primary-hover); }
|
|
67
|
-
.btn:focus-visible { outline: 2px solid var(--color-focus); outline-offset: 2px; }
|
|
68
|
-
.btn:active:not(:disabled) { transform: scale(0.97); }
|
|
69
|
-
.btn:disabled, .btn--disabled { opacity: 0.4; cursor: not-allowed; }
|
|
70
|
-
.btn--error { border-color: var(--color-error); color: var(--color-error); }
|
|
71
|
-
.btn--success { background: var(--color-success); }
|
|
72
|
-
```
|
|
73
|
-
|
|
74
|
-
### DO / DON'T
|
|
75
|
-
|
|
76
|
-
- DO define all 8 states in your design system before building components.
|
|
77
|
-
- DO use CSS custom properties so states share a single token source.
|
|
78
|
-
- DON'T use `pointer-events: none` as the sole disabled mechanism — it does not remove the element from tab order.
|
|
79
|
-
- DON'T rely on color alone to communicate state — pair color with an icon or label change.
|
|
80
|
-
- DON'T skip the active state; it is the only immediate feedback on slow networks.
|
|
81
|
-
|
|
82
|
-
---
|
|
83
|
-
|
|
84
|
-
## Focus Management
|
|
85
|
-
|
|
86
|
-
### Visible Focus Indicators
|
|
87
|
-
|
|
88
|
-
The browser default outline is intentionally visible. Never remove it without replacing it.
|
|
89
|
-
|
|
90
|
-
```css
|
|
91
|
-
/* Wrong: removes focus for everyone */
|
|
92
|
-
*:focus { outline: none; }
|
|
93
|
-
|
|
94
|
-
/* Correct: hides outline on mouse click, keeps it for keyboard */
|
|
95
|
-
*:focus-visible {
|
|
96
|
-
outline: 2px solid var(--color-focus, #0066cc);
|
|
97
|
-
outline-offset: 2px;
|
|
98
|
-
border-radius: 2px;
|
|
99
|
-
}
|
|
100
|
-
```
|
|
101
|
-
|
|
102
|
-
A minimum 3:1 contrast ratio between the focus indicator and adjacent colors is required by WCAG 2.2 (Success Criterion 2.4.11).
|
|
103
|
-
|
|
104
|
-
### Skip Links
|
|
105
|
-
|
|
106
|
-
The first focusable element on every page must be a skip link that jumps past repeated navigation.
|
|
107
|
-
|
|
108
|
-
```tsx
|
|
109
|
-
export function SkipLink() {
|
|
110
|
-
return (
|
|
111
|
-
<a
|
|
112
|
-
href="#main-content"
|
|
113
|
-
className="skip-link"
|
|
114
|
-
>
|
|
115
|
-
Skip to main content
|
|
116
|
-
</a>
|
|
117
|
-
);
|
|
118
|
-
}
|
|
119
|
-
```
|
|
120
|
-
|
|
121
|
-
```css
|
|
122
|
-
.skip-link {
|
|
123
|
-
position: absolute;
|
|
124
|
-
top: -100%;
|
|
125
|
-
left: 1rem;
|
|
126
|
-
z-index: 9999;
|
|
127
|
-
padding: 0.5rem 1rem;
|
|
128
|
-
background: var(--color-surface);
|
|
129
|
-
border: 2px solid var(--color-focus);
|
|
130
|
-
}
|
|
131
|
-
.skip-link:focus { top: 1rem; }
|
|
132
|
-
```
|
|
133
|
-
|
|
134
|
-
### Programmatic Focus Management
|
|
135
|
-
|
|
136
|
-
When content changes dynamically — modal opens, error appears, route changes — move focus deliberately.
|
|
137
|
-
|
|
138
|
-
```tsx
|
|
139
|
-
import { useEffect, useRef } from 'react';
|
|
140
|
-
|
|
141
|
-
function ErrorMessage({ message }: { message: string }) {
|
|
142
|
-
const ref = useRef<HTMLDivElement>(null);
|
|
143
|
-
|
|
144
|
-
useEffect(() => {
|
|
145
|
-
if (message) ref.current?.focus();
|
|
146
|
-
}, [message]);
|
|
147
|
-
|
|
148
|
-
return (
|
|
149
|
-
<div
|
|
150
|
-
ref={ref}
|
|
151
|
-
role="alert"
|
|
152
|
-
tabIndex={-1}
|
|
153
|
-
className="error-message"
|
|
154
|
-
>
|
|
155
|
-
{message}
|
|
156
|
-
</div>
|
|
157
|
-
);
|
|
158
|
-
}
|
|
159
|
-
```
|
|
160
|
-
|
|
161
|
-
### DO / DON'T
|
|
162
|
-
|
|
163
|
-
- DO use `:focus-visible` exclusively — never `:focus` for visual indicators.
|
|
164
|
-
- DO include a skip link as the first interactive element on every page.
|
|
165
|
-
- DON'T use `tabIndex` values above 0; they break the natural tab order.
|
|
166
|
-
- DON'T move focus on scroll or hover — only move it in response to user-initiated actions.
|
|
167
|
-
|
|
168
|
-
---
|
|
169
|
-
|
|
170
|
-
## Form Design
|
|
171
|
-
|
|
172
|
-
### Labels Are Non-Negotiable
|
|
173
|
-
|
|
174
|
-
Every input requires a visible, persistent label. Placeholder text is not a label — it disappears on input and has low contrast.
|
|
175
|
-
|
|
176
|
-
```tsx
|
|
177
|
-
interface FieldProps {
|
|
178
|
-
id: string;
|
|
179
|
-
label: string;
|
|
180
|
-
error?: string;
|
|
181
|
-
required?: boolean;
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
export function TextField({ id, label, error, required, ...inputProps }: FieldProps) {
|
|
185
|
-
const errorId = `${id}-error`;
|
|
186
|
-
return (
|
|
187
|
-
<div className="field">
|
|
188
|
-
<label htmlFor={id} className="field__label">
|
|
189
|
-
{label}
|
|
190
|
-
{required && <span aria-hidden="true" className="field__required"> *</span>}
|
|
191
|
-
</label>
|
|
192
|
-
<input
|
|
193
|
-
id={id}
|
|
194
|
-
aria-describedby={error ? errorId : undefined}
|
|
195
|
-
aria-invalid={!!error}
|
|
196
|
-
aria-required={required}
|
|
197
|
-
className={`field__input ${error ? 'field__input--error' : ''}`}
|
|
198
|
-
{...inputProps}
|
|
199
|
-
/>
|
|
200
|
-
{error && (
|
|
201
|
-
<p id={errorId} role="alert" className="field__error">
|
|
202
|
-
{error}
|
|
203
|
-
</p>
|
|
204
|
-
)}
|
|
205
|
-
</div>
|
|
206
|
-
);
|
|
207
|
-
}
|
|
208
|
-
```
|
|
209
|
-
|
|
210
|
-
### Inline Validation Timing
|
|
211
|
-
|
|
212
|
-
Validate on blur (when the user leaves the field), not on every keystroke. Re-validate on change after the first error is shown. This prevents premature errors while still giving fast feedback.
|
|
213
|
-
|
|
214
|
-
```tsx
|
|
215
|
-
function useFieldValidation(validate: (v: string) => string | undefined) {
|
|
216
|
-
const [value, setValue] = useState('');
|
|
217
|
-
const [error, setError] = useState<string | undefined>();
|
|
218
|
-
const [touched, setTouched] = useState(false);
|
|
219
|
-
|
|
220
|
-
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
221
|
-
setValue(e.target.value);
|
|
222
|
-
if (touched) setError(validate(e.target.value));
|
|
223
|
-
};
|
|
224
|
-
|
|
225
|
-
const handleBlur = () => {
|
|
226
|
-
setTouched(true);
|
|
227
|
-
setError(validate(value));
|
|
228
|
-
};
|
|
229
|
-
|
|
230
|
-
return { value, error, handleChange, handleBlur };
|
|
231
|
-
}
|
|
232
|
-
```
|
|
233
|
-
|
|
234
|
-
### Submit Button States
|
|
235
|
-
|
|
236
|
-
The submit button must reflect loading and result states to prevent double-submission.
|
|
237
|
-
|
|
238
|
-
```tsx
|
|
239
|
-
type SubmitState = 'idle' | 'submitting' | 'submitted' | 'error';
|
|
240
|
-
|
|
241
|
-
function SubmitButton({ state }: { state: SubmitState }) {
|
|
242
|
-
const labels: Record<SubmitState, string> = {
|
|
243
|
-
idle: 'Submit',
|
|
244
|
-
submitting: 'Submitting…',
|
|
245
|
-
submitted: 'Submitted',
|
|
246
|
-
error: 'Try again',
|
|
247
|
-
};
|
|
248
|
-
return (
|
|
249
|
-
<button type="submit" disabled={state === 'submitting'} aria-busy={state === 'submitting'}>
|
|
250
|
-
{labels[state]}
|
|
251
|
-
</button>
|
|
252
|
-
);
|
|
253
|
-
}
|
|
254
|
-
```
|
|
255
|
-
|
|
256
|
-
### DO / DON'T
|
|
257
|
-
|
|
258
|
-
- DO place error messages directly below the relevant field, not in a summary at the top.
|
|
259
|
-
- DO use `aria-describedby` to associate the error with the input programmatically.
|
|
260
|
-
- DON'T use placeholder text as a substitute for labels.
|
|
261
|
-
- DON'T validate on every keystroke for the first visit to a field.
|
|
262
|
-
- DON'T disable the submit button before the user attempts submission — let them try and show specific errors.
|
|
263
|
-
|
|
264
|
-
---
|
|
265
|
-
|
|
266
|
-
## Modal Patterns
|
|
267
|
-
|
|
268
|
-
### Focus Trap
|
|
269
|
-
|
|
270
|
-
When a modal opens, focus must stay inside it. Tabbing past the last focusable element wraps back to the first.
|
|
271
|
-
|
|
272
|
-
```tsx
|
|
273
|
-
import { useEffect, useRef } from 'react';
|
|
274
|
-
|
|
275
|
-
const FOCUSABLE = 'a, button, input, select, textarea, [tabindex]:not([tabindex="-1"])';
|
|
276
|
-
|
|
277
|
-
export function Modal({ isOpen, onClose, children }: ModalProps) {
|
|
278
|
-
const dialogRef = useRef<HTMLDivElement>(null);
|
|
279
|
-
|
|
280
|
-
useEffect(() => {
|
|
281
|
-
if (!isOpen) return;
|
|
282
|
-
|
|
283
|
-
const dialog = dialogRef.current;
|
|
284
|
-
if (!dialog) return;
|
|
285
|
-
|
|
286
|
-
const focusable = Array.from(dialog.querySelectorAll<HTMLElement>(FOCUSABLE));
|
|
287
|
-
const first = focusable[0];
|
|
288
|
-
const last = focusable[focusable.length - 1];
|
|
289
|
-
|
|
290
|
-
first?.focus();
|
|
291
|
-
|
|
292
|
-
const trap = (e: KeyboardEvent) => {
|
|
293
|
-
if (e.key !== 'Tab') return;
|
|
294
|
-
if (e.shiftKey && document.activeElement === first) {
|
|
295
|
-
e.preventDefault();
|
|
296
|
-
last.focus();
|
|
297
|
-
} else if (!e.shiftKey && document.activeElement === last) {
|
|
298
|
-
e.preventDefault();
|
|
299
|
-
first.focus();
|
|
300
|
-
}
|
|
301
|
-
};
|
|
302
|
-
|
|
303
|
-
const escape = (e: KeyboardEvent) => {
|
|
304
|
-
if (e.key === 'Escape') onClose();
|
|
305
|
-
};
|
|
306
|
-
|
|
307
|
-
document.addEventListener('keydown', trap);
|
|
308
|
-
document.addEventListener('keydown', escape);
|
|
309
|
-
return () => {
|
|
310
|
-
document.removeEventListener('keydown', trap);
|
|
311
|
-
document.removeEventListener('keydown', escape);
|
|
312
|
-
};
|
|
313
|
-
}, [isOpen, onClose]);
|
|
314
|
-
|
|
315
|
-
if (!isOpen) return null;
|
|
316
|
-
|
|
317
|
-
return (
|
|
318
|
-
<>
|
|
319
|
-
<div className="modal-backdrop" onClick={onClose} aria-hidden="true" />
|
|
320
|
-
<div
|
|
321
|
-
ref={dialogRef}
|
|
322
|
-
role="dialog"
|
|
323
|
-
aria-modal="true"
|
|
324
|
-
className="modal"
|
|
325
|
-
>
|
|
326
|
-
{children}
|
|
327
|
-
</div>
|
|
328
|
-
</>
|
|
329
|
-
);
|
|
330
|
-
}
|
|
331
|
-
```
|
|
332
|
-
|
|
333
|
-
### Scroll Lock
|
|
334
|
-
|
|
335
|
-
Prevent the page from scrolling behind an open modal.
|
|
336
|
-
|
|
337
|
-
```css
|
|
338
|
-
body.modal-open {
|
|
339
|
-
overflow: hidden;
|
|
340
|
-
/* Compensate for scrollbar width to prevent layout shift */
|
|
341
|
-
padding-right: var(--scrollbar-width, 0);
|
|
342
|
-
}
|
|
343
|
-
```
|
|
344
|
-
|
|
345
|
-
### DO / DON'T
|
|
346
|
-
|
|
347
|
-
- DO return focus to the trigger element when the modal closes.
|
|
348
|
-
- DO lock scroll on `<body>` when a modal is open.
|
|
349
|
-
- DON'T make backdrop click the only way to close — always support Escape.
|
|
350
|
-
- DON'T autofocus a destructive action (e.g., "Delete") as the first focusable element.
|
|
351
|
-
|
|
352
|
-
---
|
|
353
|
-
|
|
354
|
-
## Dropdown and Menu Patterns
|
|
355
|
-
|
|
356
|
-
### Keyboard Navigation
|
|
357
|
-
|
|
358
|
-
ARIA `menu` and `menuitem` roles carry expected keyboard contracts: arrow keys move between items, Enter/Space activate, Escape closes, and Home/End jump to first/last.
|
|
359
|
-
|
|
360
|
-
```tsx
|
|
361
|
-
function useMenuKeyboard(items: HTMLElement[], onClose: () => void) {
|
|
362
|
-
return (e: React.KeyboardEvent) => {
|
|
363
|
-
const current = document.activeElement as HTMLElement;
|
|
364
|
-
const idx = items.indexOf(current);
|
|
365
|
-
|
|
366
|
-
switch (e.key) {
|
|
367
|
-
case 'ArrowDown':
|
|
368
|
-
e.preventDefault();
|
|
369
|
-
items[(idx + 1) % items.length]?.focus();
|
|
370
|
-
break;
|
|
371
|
-
case 'ArrowUp':
|
|
372
|
-
e.preventDefault();
|
|
373
|
-
items[(idx - 1 + items.length) % items.length]?.focus();
|
|
374
|
-
break;
|
|
375
|
-
case 'Home':
|
|
376
|
-
e.preventDefault();
|
|
377
|
-
items[0]?.focus();
|
|
378
|
-
break;
|
|
379
|
-
case 'End':
|
|
380
|
-
e.preventDefault();
|
|
381
|
-
items[items.length - 1]?.focus();
|
|
382
|
-
break;
|
|
383
|
-
case 'Escape':
|
|
384
|
-
onClose();
|
|
385
|
-
break;
|
|
386
|
-
}
|
|
387
|
-
};
|
|
388
|
-
}
|
|
389
|
-
```
|
|
390
|
-
|
|
391
|
-
### Typeahead Search
|
|
392
|
-
|
|
393
|
-
Users expect to type a letter to jump to the matching item in a list.
|
|
394
|
-
|
|
395
|
-
```tsx
|
|
396
|
-
function useTypeahead(items: string[]) {
|
|
397
|
-
const buffer = useRef('');
|
|
398
|
-
const timer = useRef<ReturnType<typeof setTimeout>>();
|
|
399
|
-
|
|
400
|
-
return (key: string): number => {
|
|
401
|
-
clearTimeout(timer.current);
|
|
402
|
-
buffer.current += key.toLowerCase();
|
|
403
|
-
timer.current = setTimeout(() => { buffer.current = ''; }, 500);
|
|
404
|
-
return items.findIndex(item => item.toLowerCase().startsWith(buffer.current));
|
|
405
|
-
};
|
|
406
|
-
}
|
|
407
|
-
```
|
|
408
|
-
|
|
409
|
-
### Anchor Positioning
|
|
410
|
-
|
|
411
|
-
Position the dropdown below its trigger by default. Flip above if there is not enough viewport space below.
|
|
412
|
-
|
|
413
|
-
```css
|
|
414
|
-
.dropdown {
|
|
415
|
-
position: absolute;
|
|
416
|
-
top: calc(100% + 4px);
|
|
417
|
-
left: 0;
|
|
418
|
-
min-width: 100%;
|
|
419
|
-
z-index: var(--z-dropdown, 100);
|
|
420
|
-
}
|
|
421
|
-
```
|
|
422
|
-
|
|
423
|
-
### DO / DON'T
|
|
424
|
-
|
|
425
|
-
- DO implement full arrow-key navigation in every menu and listbox.
|
|
426
|
-
- DO support typeahead in long lists (more than 7 items).
|
|
427
|
-
- DON'T close the menu on blur from any item — close only when focus leaves the entire menu widget.
|
|
428
|
-
- DON'T open a dropdown on hover for primary navigation; hover reveals are invisible to keyboard and touch users.
|
|
429
|
-
|
|
430
|
-
---
|
|
431
|
-
|
|
432
|
-
## Touch Targets
|
|
433
|
-
|
|
434
|
-
### Minimum Size Requirements
|
|
435
|
-
|
|
436
|
-
WCAG 2.5.5 (Level AAA) and WCAG 2.5.8 (Level AA, 2.2) require a minimum 24×24px target size with adequate spacing. Apple HIG and Material Design both recommend 44×44px as a practical minimum.
|
|
437
|
-
|
|
438
|
-
```css
|
|
439
|
-
.touch-target {
|
|
440
|
-
min-width: 44px;
|
|
441
|
-
min-height: 44px;
|
|
442
|
-
display: inline-flex;
|
|
443
|
-
align-items: center;
|
|
444
|
-
justify-content: center;
|
|
445
|
-
}
|
|
446
|
-
```
|
|
447
|
-
|
|
448
|
-
When the visual element must be smaller (e.g., a 16px icon), expand the hit area with padding or a pseudo-element.
|
|
449
|
-
|
|
450
|
-
```css
|
|
451
|
-
.icon-button {
|
|
452
|
-
position: relative;
|
|
453
|
-
width: 20px;
|
|
454
|
-
height: 20px;
|
|
455
|
-
}
|
|
456
|
-
.icon-button::before {
|
|
457
|
-
content: '';
|
|
458
|
-
position: absolute;
|
|
459
|
-
inset: -12px; /* extends 12px on each side → 44px total */
|
|
460
|
-
}
|
|
461
|
-
```
|
|
462
|
-
|
|
463
|
-
### Spacing Between Targets
|
|
464
|
-
|
|
465
|
-
Adjacent targets need at least 8px of gap so accidental activation is unlikely.
|
|
466
|
-
|
|
467
|
-
```css
|
|
468
|
-
.toolbar {
|
|
469
|
-
display: flex;
|
|
470
|
-
gap: 8px;
|
|
471
|
-
}
|
|
472
|
-
```
|
|
473
|
-
|
|
474
|
-
### DO / DON'T
|
|
475
|
-
|
|
476
|
-
- DO expand hit areas with padding or pseudo-elements rather than enlarging the visual element.
|
|
477
|
-
- DO maintain at least 8px spacing between adjacent interactive elements.
|
|
478
|
-
- DON'T place two destructive actions (e.g., "Delete" and "Archive") adjacent with minimal spacing.
|
|
479
|
-
- DON'T measure touch targets by their visual size — measure the actual interactive area.
|
|
480
|
-
|
|
481
|
-
---
|
|
482
|
-
|
|
483
|
-
## Loading Patterns
|
|
484
|
-
|
|
485
|
-
### Choosing the Right Pattern
|
|
486
|
-
|
|
487
|
-
Three loading patterns serve different situations.
|
|
488
|
-
|
|
489
|
-
**Skeleton screens** — use when you know the shape of the incoming content (a card, a table row, a list). They set accurate spatial expectations and feel faster because they show structure immediately.
|
|
490
|
-
|
|
491
|
-
**Spinners** — use when you do not know the shape of the result (a search, a file upload) or when the operation is fast enough (under 300ms) that a skeleton would flash.
|
|
492
|
-
|
|
493
|
-
**Progress bars** — use only when you have a real percentage to report (file upload, batch processing). A fake animated progress bar is worse than a spinner because it lies to the user.
|
|
494
|
-
|
|
495
|
-
```tsx
|
|
496
|
-
function SkeletonCard() {
|
|
497
|
-
return (
|
|
498
|
-
<div className="skeleton-card" aria-hidden="true">
|
|
499
|
-
<div className="skeleton skeleton--avatar" />
|
|
500
|
-
<div className="skeleton skeleton--line skeleton--line-80" />
|
|
501
|
-
<div className="skeleton skeleton--line skeleton--line-60" />
|
|
502
|
-
</div>
|
|
503
|
-
);
|
|
504
|
-
}
|
|
505
|
-
```
|
|
506
|
-
|
|
507
|
-
```css
|
|
508
|
-
@keyframes shimmer {
|
|
509
|
-
from { background-position: -200% 0; }
|
|
510
|
-
to { background-position: 200% 0; }
|
|
511
|
-
}
|
|
512
|
-
|
|
513
|
-
.skeleton {
|
|
514
|
-
background: linear-gradient(
|
|
515
|
-
90deg,
|
|
516
|
-
var(--color-skeleton-base) 25%,
|
|
517
|
-
var(--color-skeleton-highlight) 50%,
|
|
518
|
-
var(--color-skeleton-base) 75%
|
|
519
|
-
);
|
|
520
|
-
background-size: 200% 100%;
|
|
521
|
-
animation: shimmer 1.4s ease infinite;
|
|
522
|
-
border-radius: 4px;
|
|
523
|
-
}
|
|
524
|
-
```
|
|
525
|
-
|
|
526
|
-
### Announcing Loading State to Screen Readers
|
|
527
|
-
|
|
528
|
-
```tsx
|
|
529
|
-
function LoadingRegion({ isLoading, children }: { isLoading: boolean; children: React.ReactNode }) {
|
|
530
|
-
return (
|
|
531
|
-
<div aria-live="polite" aria-busy={isLoading}>
|
|
532
|
-
{isLoading ? <SkeletonCard /> : children}
|
|
533
|
-
</div>
|
|
534
|
-
);
|
|
535
|
-
}
|
|
536
|
-
```
|
|
537
|
-
|
|
538
|
-
### DO / DON'T
|
|
539
|
-
|
|
540
|
-
- DO use skeleton screens for content with a known layout.
|
|
541
|
-
- DO add `aria-busy="true"` and `aria-live="polite"` to regions that load asynchronously.
|
|
542
|
-
- DON'T use a fake animated progress bar that does not reflect real progress.
|
|
543
|
-
- DON'T show a spinner for operations that complete in under 100ms — the flash is disorienting.
|
|
544
|
-
- DON'T leave a spinner visible indefinitely without a timeout and an error fallback.
|
|
1
|
+
# Interaction Design Reference
|
|
2
|
+
|
|
3
|
+
Deep reference for building interactive UI that is accessible, predictable, and polished. Covers the full lifecycle of a component's states, focus handling, form behavior, overlays, menus, touch targets, and loading feedback.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## 8 Interactive States
|
|
8
|
+
|
|
9
|
+
Every interactive element must account for eight distinct states. Missing even one breaks accessibility or trust.
|
|
10
|
+
|
|
11
|
+
### State Definitions and Implementation
|
|
12
|
+
|
|
13
|
+
**Default** — the element at rest. Provide enough visual affordance that the user knows it is interactive. Borders, shadows, and color contrast carry this signal.
|
|
14
|
+
|
|
15
|
+
**Hover** — pointer is over the element but not pressed. Communicate interactivity with a cursor change and a subtle visual shift. Never rely on hover alone to reveal essential UI.
|
|
16
|
+
|
|
17
|
+
**Focus** — keyboard or programmatic focus. Must be visible at all times for keyboard users. Use `:focus-visible` so mouse users are not shown an outline on click.
|
|
18
|
+
|
|
19
|
+
**Active** — element is being pressed (mousedown / touchstart). Provide immediate feedback: a slight scale-down or darker background confirms the press registered.
|
|
20
|
+
|
|
21
|
+
**Disabled** — element exists but cannot be interacted with. Reduce opacity, change cursor to `not-allowed`, and remove from tab order with `disabled` attribute (not just `aria-disabled`).
|
|
22
|
+
|
|
23
|
+
**Loading** — action has been triggered and the system is working. Replace interactive content with a spinner or skeleton. Disable the trigger to prevent double-submission.
|
|
24
|
+
|
|
25
|
+
**Error** — the action failed or input is invalid. Show a message in red near the source. Use `aria-describedby` to associate the message with the input.
|
|
26
|
+
|
|
27
|
+
**Success** — the action completed. Provide a brief confirmation (green checkmark, toast, inline message). Auto-dismiss non-critical confirmations after 3–5 seconds.
|
|
28
|
+
|
|
29
|
+
```tsx
|
|
30
|
+
// Button covering all 8 states
|
|
31
|
+
type ButtonState = 'default' | 'loading' | 'error' | 'success';
|
|
32
|
+
|
|
33
|
+
interface ButtonProps {
|
|
34
|
+
label: string;
|
|
35
|
+
state?: ButtonState;
|
|
36
|
+
disabled?: boolean;
|
|
37
|
+
onClick: () => void;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function Button({ label, state = 'default', disabled = false, onClick }: ButtonProps) {
|
|
41
|
+
const isLoading = state === 'loading';
|
|
42
|
+
const isDisabled = disabled || isLoading;
|
|
43
|
+
|
|
44
|
+
return (
|
|
45
|
+
<button
|
|
46
|
+
onClick={onClick}
|
|
47
|
+
disabled={isDisabled}
|
|
48
|
+
aria-busy={isLoading}
|
|
49
|
+
className={[
|
|
50
|
+
'btn',
|
|
51
|
+
`btn--${state}`,
|
|
52
|
+
isDisabled ? 'btn--disabled' : '',
|
|
53
|
+
].join(' ')}
|
|
54
|
+
>
|
|
55
|
+
{isLoading ? <Spinner size={16} /> : label}
|
|
56
|
+
</button>
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
```css
|
|
62
|
+
.btn {
|
|
63
|
+
cursor: pointer;
|
|
64
|
+
transition: background 120ms ease, transform 80ms ease, opacity 120ms ease;
|
|
65
|
+
}
|
|
66
|
+
.btn:hover:not(:disabled) { background: var(--color-primary-hover); }
|
|
67
|
+
.btn:focus-visible { outline: 2px solid var(--color-focus); outline-offset: 2px; }
|
|
68
|
+
.btn:active:not(:disabled) { transform: scale(0.97); }
|
|
69
|
+
.btn:disabled, .btn--disabled { opacity: 0.4; cursor: not-allowed; }
|
|
70
|
+
.btn--error { border-color: var(--color-error); color: var(--color-error); }
|
|
71
|
+
.btn--success { background: var(--color-success); }
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### DO / DON'T
|
|
75
|
+
|
|
76
|
+
- DO define all 8 states in your design system before building components.
|
|
77
|
+
- DO use CSS custom properties so states share a single token source.
|
|
78
|
+
- DON'T use `pointer-events: none` as the sole disabled mechanism — it does not remove the element from tab order.
|
|
79
|
+
- DON'T rely on color alone to communicate state — pair color with an icon or label change.
|
|
80
|
+
- DON'T skip the active state; it is the only immediate feedback on slow networks.
|
|
81
|
+
|
|
82
|
+
---
|
|
83
|
+
|
|
84
|
+
## Focus Management
|
|
85
|
+
|
|
86
|
+
### Visible Focus Indicators
|
|
87
|
+
|
|
88
|
+
The browser default outline is intentionally visible. Never remove it without replacing it.
|
|
89
|
+
|
|
90
|
+
```css
|
|
91
|
+
/* Wrong: removes focus for everyone */
|
|
92
|
+
*:focus { outline: none; }
|
|
93
|
+
|
|
94
|
+
/* Correct: hides outline on mouse click, keeps it for keyboard */
|
|
95
|
+
*:focus-visible {
|
|
96
|
+
outline: 2px solid var(--color-focus, #0066cc);
|
|
97
|
+
outline-offset: 2px;
|
|
98
|
+
border-radius: 2px;
|
|
99
|
+
}
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
A minimum 3:1 contrast ratio between the focus indicator and adjacent colors is required by WCAG 2.2 (Success Criterion 2.4.11).
|
|
103
|
+
|
|
104
|
+
### Skip Links
|
|
105
|
+
|
|
106
|
+
The first focusable element on every page must be a skip link that jumps past repeated navigation.
|
|
107
|
+
|
|
108
|
+
```tsx
|
|
109
|
+
export function SkipLink() {
|
|
110
|
+
return (
|
|
111
|
+
<a
|
|
112
|
+
href="#main-content"
|
|
113
|
+
className="skip-link"
|
|
114
|
+
>
|
|
115
|
+
Skip to main content
|
|
116
|
+
</a>
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
```css
|
|
122
|
+
.skip-link {
|
|
123
|
+
position: absolute;
|
|
124
|
+
top: -100%;
|
|
125
|
+
left: 1rem;
|
|
126
|
+
z-index: 9999;
|
|
127
|
+
padding: 0.5rem 1rem;
|
|
128
|
+
background: var(--color-surface);
|
|
129
|
+
border: 2px solid var(--color-focus);
|
|
130
|
+
}
|
|
131
|
+
.skip-link:focus { top: 1rem; }
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
### Programmatic Focus Management
|
|
135
|
+
|
|
136
|
+
When content changes dynamically — modal opens, error appears, route changes — move focus deliberately.
|
|
137
|
+
|
|
138
|
+
```tsx
|
|
139
|
+
import { useEffect, useRef } from 'react';
|
|
140
|
+
|
|
141
|
+
function ErrorMessage({ message }: { message: string }) {
|
|
142
|
+
const ref = useRef<HTMLDivElement>(null);
|
|
143
|
+
|
|
144
|
+
useEffect(() => {
|
|
145
|
+
if (message) ref.current?.focus();
|
|
146
|
+
}, [message]);
|
|
147
|
+
|
|
148
|
+
return (
|
|
149
|
+
<div
|
|
150
|
+
ref={ref}
|
|
151
|
+
role="alert"
|
|
152
|
+
tabIndex={-1}
|
|
153
|
+
className="error-message"
|
|
154
|
+
>
|
|
155
|
+
{message}
|
|
156
|
+
</div>
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
### DO / DON'T
|
|
162
|
+
|
|
163
|
+
- DO use `:focus-visible` exclusively — never `:focus` for visual indicators.
|
|
164
|
+
- DO include a skip link as the first interactive element on every page.
|
|
165
|
+
- DON'T use `tabIndex` values above 0; they break the natural tab order.
|
|
166
|
+
- DON'T move focus on scroll or hover — only move it in response to user-initiated actions.
|
|
167
|
+
|
|
168
|
+
---
|
|
169
|
+
|
|
170
|
+
## Form Design
|
|
171
|
+
|
|
172
|
+
### Labels Are Non-Negotiable
|
|
173
|
+
|
|
174
|
+
Every input requires a visible, persistent label. Placeholder text is not a label — it disappears on input and has low contrast.
|
|
175
|
+
|
|
176
|
+
```tsx
|
|
177
|
+
interface FieldProps {
|
|
178
|
+
id: string;
|
|
179
|
+
label: string;
|
|
180
|
+
error?: string;
|
|
181
|
+
required?: boolean;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export function TextField({ id, label, error, required, ...inputProps }: FieldProps) {
|
|
185
|
+
const errorId = `${id}-error`;
|
|
186
|
+
return (
|
|
187
|
+
<div className="field">
|
|
188
|
+
<label htmlFor={id} className="field__label">
|
|
189
|
+
{label}
|
|
190
|
+
{required && <span aria-hidden="true" className="field__required"> *</span>}
|
|
191
|
+
</label>
|
|
192
|
+
<input
|
|
193
|
+
id={id}
|
|
194
|
+
aria-describedby={error ? errorId : undefined}
|
|
195
|
+
aria-invalid={!!error}
|
|
196
|
+
aria-required={required}
|
|
197
|
+
className={`field__input ${error ? 'field__input--error' : ''}`}
|
|
198
|
+
{...inputProps}
|
|
199
|
+
/>
|
|
200
|
+
{error && (
|
|
201
|
+
<p id={errorId} role="alert" className="field__error">
|
|
202
|
+
{error}
|
|
203
|
+
</p>
|
|
204
|
+
)}
|
|
205
|
+
</div>
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
### Inline Validation Timing
|
|
211
|
+
|
|
212
|
+
Validate on blur (when the user leaves the field), not on every keystroke. Re-validate on change after the first error is shown. This prevents premature errors while still giving fast feedback.
|
|
213
|
+
|
|
214
|
+
```tsx
|
|
215
|
+
function useFieldValidation(validate: (v: string) => string | undefined) {
|
|
216
|
+
const [value, setValue] = useState('');
|
|
217
|
+
const [error, setError] = useState<string | undefined>();
|
|
218
|
+
const [touched, setTouched] = useState(false);
|
|
219
|
+
|
|
220
|
+
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
221
|
+
setValue(e.target.value);
|
|
222
|
+
if (touched) setError(validate(e.target.value));
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
const handleBlur = () => {
|
|
226
|
+
setTouched(true);
|
|
227
|
+
setError(validate(value));
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
return { value, error, handleChange, handleBlur };
|
|
231
|
+
}
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
### Submit Button States
|
|
235
|
+
|
|
236
|
+
The submit button must reflect loading and result states to prevent double-submission.
|
|
237
|
+
|
|
238
|
+
```tsx
|
|
239
|
+
type SubmitState = 'idle' | 'submitting' | 'submitted' | 'error';
|
|
240
|
+
|
|
241
|
+
function SubmitButton({ state }: { state: SubmitState }) {
|
|
242
|
+
const labels: Record<SubmitState, string> = {
|
|
243
|
+
idle: 'Submit',
|
|
244
|
+
submitting: 'Submitting…',
|
|
245
|
+
submitted: 'Submitted',
|
|
246
|
+
error: 'Try again',
|
|
247
|
+
};
|
|
248
|
+
return (
|
|
249
|
+
<button type="submit" disabled={state === 'submitting'} aria-busy={state === 'submitting'}>
|
|
250
|
+
{labels[state]}
|
|
251
|
+
</button>
|
|
252
|
+
);
|
|
253
|
+
}
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
### DO / DON'T
|
|
257
|
+
|
|
258
|
+
- DO place error messages directly below the relevant field, not in a summary at the top.
|
|
259
|
+
- DO use `aria-describedby` to associate the error with the input programmatically.
|
|
260
|
+
- DON'T use placeholder text as a substitute for labels.
|
|
261
|
+
- DON'T validate on every keystroke for the first visit to a field.
|
|
262
|
+
- DON'T disable the submit button before the user attempts submission — let them try and show specific errors.
|
|
263
|
+
|
|
264
|
+
---
|
|
265
|
+
|
|
266
|
+
## Modal Patterns
|
|
267
|
+
|
|
268
|
+
### Focus Trap
|
|
269
|
+
|
|
270
|
+
When a modal opens, focus must stay inside it. Tabbing past the last focusable element wraps back to the first.
|
|
271
|
+
|
|
272
|
+
```tsx
|
|
273
|
+
import { useEffect, useRef } from 'react';
|
|
274
|
+
|
|
275
|
+
const FOCUSABLE = 'a, button, input, select, textarea, [tabindex]:not([tabindex="-1"])';
|
|
276
|
+
|
|
277
|
+
export function Modal({ isOpen, onClose, children }: ModalProps) {
|
|
278
|
+
const dialogRef = useRef<HTMLDivElement>(null);
|
|
279
|
+
|
|
280
|
+
useEffect(() => {
|
|
281
|
+
if (!isOpen) return;
|
|
282
|
+
|
|
283
|
+
const dialog = dialogRef.current;
|
|
284
|
+
if (!dialog) return;
|
|
285
|
+
|
|
286
|
+
const focusable = Array.from(dialog.querySelectorAll<HTMLElement>(FOCUSABLE));
|
|
287
|
+
const first = focusable[0];
|
|
288
|
+
const last = focusable[focusable.length - 1];
|
|
289
|
+
|
|
290
|
+
first?.focus();
|
|
291
|
+
|
|
292
|
+
const trap = (e: KeyboardEvent) => {
|
|
293
|
+
if (e.key !== 'Tab') return;
|
|
294
|
+
if (e.shiftKey && document.activeElement === first) {
|
|
295
|
+
e.preventDefault();
|
|
296
|
+
last.focus();
|
|
297
|
+
} else if (!e.shiftKey && document.activeElement === last) {
|
|
298
|
+
e.preventDefault();
|
|
299
|
+
first.focus();
|
|
300
|
+
}
|
|
301
|
+
};
|
|
302
|
+
|
|
303
|
+
const escape = (e: KeyboardEvent) => {
|
|
304
|
+
if (e.key === 'Escape') onClose();
|
|
305
|
+
};
|
|
306
|
+
|
|
307
|
+
document.addEventListener('keydown', trap);
|
|
308
|
+
document.addEventListener('keydown', escape);
|
|
309
|
+
return () => {
|
|
310
|
+
document.removeEventListener('keydown', trap);
|
|
311
|
+
document.removeEventListener('keydown', escape);
|
|
312
|
+
};
|
|
313
|
+
}, [isOpen, onClose]);
|
|
314
|
+
|
|
315
|
+
if (!isOpen) return null;
|
|
316
|
+
|
|
317
|
+
return (
|
|
318
|
+
<>
|
|
319
|
+
<div className="modal-backdrop" onClick={onClose} aria-hidden="true" />
|
|
320
|
+
<div
|
|
321
|
+
ref={dialogRef}
|
|
322
|
+
role="dialog"
|
|
323
|
+
aria-modal="true"
|
|
324
|
+
className="modal"
|
|
325
|
+
>
|
|
326
|
+
{children}
|
|
327
|
+
</div>
|
|
328
|
+
</>
|
|
329
|
+
);
|
|
330
|
+
}
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
### Scroll Lock
|
|
334
|
+
|
|
335
|
+
Prevent the page from scrolling behind an open modal.
|
|
336
|
+
|
|
337
|
+
```css
|
|
338
|
+
body.modal-open {
|
|
339
|
+
overflow: hidden;
|
|
340
|
+
/* Compensate for scrollbar width to prevent layout shift */
|
|
341
|
+
padding-right: var(--scrollbar-width, 0);
|
|
342
|
+
}
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
### DO / DON'T
|
|
346
|
+
|
|
347
|
+
- DO return focus to the trigger element when the modal closes.
|
|
348
|
+
- DO lock scroll on `<body>` when a modal is open.
|
|
349
|
+
- DON'T make backdrop click the only way to close — always support Escape.
|
|
350
|
+
- DON'T autofocus a destructive action (e.g., "Delete") as the first focusable element.
|
|
351
|
+
|
|
352
|
+
---
|
|
353
|
+
|
|
354
|
+
## Dropdown and Menu Patterns
|
|
355
|
+
|
|
356
|
+
### Keyboard Navigation
|
|
357
|
+
|
|
358
|
+
ARIA `menu` and `menuitem` roles carry expected keyboard contracts: arrow keys move between items, Enter/Space activate, Escape closes, and Home/End jump to first/last.
|
|
359
|
+
|
|
360
|
+
```tsx
|
|
361
|
+
function useMenuKeyboard(items: HTMLElement[], onClose: () => void) {
|
|
362
|
+
return (e: React.KeyboardEvent) => {
|
|
363
|
+
const current = document.activeElement as HTMLElement;
|
|
364
|
+
const idx = items.indexOf(current);
|
|
365
|
+
|
|
366
|
+
switch (e.key) {
|
|
367
|
+
case 'ArrowDown':
|
|
368
|
+
e.preventDefault();
|
|
369
|
+
items[(idx + 1) % items.length]?.focus();
|
|
370
|
+
break;
|
|
371
|
+
case 'ArrowUp':
|
|
372
|
+
e.preventDefault();
|
|
373
|
+
items[(idx - 1 + items.length) % items.length]?.focus();
|
|
374
|
+
break;
|
|
375
|
+
case 'Home':
|
|
376
|
+
e.preventDefault();
|
|
377
|
+
items[0]?.focus();
|
|
378
|
+
break;
|
|
379
|
+
case 'End':
|
|
380
|
+
e.preventDefault();
|
|
381
|
+
items[items.length - 1]?.focus();
|
|
382
|
+
break;
|
|
383
|
+
case 'Escape':
|
|
384
|
+
onClose();
|
|
385
|
+
break;
|
|
386
|
+
}
|
|
387
|
+
};
|
|
388
|
+
}
|
|
389
|
+
```
|
|
390
|
+
|
|
391
|
+
### Typeahead Search
|
|
392
|
+
|
|
393
|
+
Users expect to type a letter to jump to the matching item in a list.
|
|
394
|
+
|
|
395
|
+
```tsx
|
|
396
|
+
function useTypeahead(items: string[]) {
|
|
397
|
+
const buffer = useRef('');
|
|
398
|
+
const timer = useRef<ReturnType<typeof setTimeout>>();
|
|
399
|
+
|
|
400
|
+
return (key: string): number => {
|
|
401
|
+
clearTimeout(timer.current);
|
|
402
|
+
buffer.current += key.toLowerCase();
|
|
403
|
+
timer.current = setTimeout(() => { buffer.current = ''; }, 500);
|
|
404
|
+
return items.findIndex(item => item.toLowerCase().startsWith(buffer.current));
|
|
405
|
+
};
|
|
406
|
+
}
|
|
407
|
+
```
|
|
408
|
+
|
|
409
|
+
### Anchor Positioning
|
|
410
|
+
|
|
411
|
+
Position the dropdown below its trigger by default. Flip above if there is not enough viewport space below.
|
|
412
|
+
|
|
413
|
+
```css
|
|
414
|
+
.dropdown {
|
|
415
|
+
position: absolute;
|
|
416
|
+
top: calc(100% + 4px);
|
|
417
|
+
left: 0;
|
|
418
|
+
min-width: 100%;
|
|
419
|
+
z-index: var(--z-dropdown, 100);
|
|
420
|
+
}
|
|
421
|
+
```
|
|
422
|
+
|
|
423
|
+
### DO / DON'T
|
|
424
|
+
|
|
425
|
+
- DO implement full arrow-key navigation in every menu and listbox.
|
|
426
|
+
- DO support typeahead in long lists (more than 7 items).
|
|
427
|
+
- DON'T close the menu on blur from any item — close only when focus leaves the entire menu widget.
|
|
428
|
+
- DON'T open a dropdown on hover for primary navigation; hover reveals are invisible to keyboard and touch users.
|
|
429
|
+
|
|
430
|
+
---
|
|
431
|
+
|
|
432
|
+
## Touch Targets
|
|
433
|
+
|
|
434
|
+
### Minimum Size Requirements
|
|
435
|
+
|
|
436
|
+
WCAG 2.5.5 (Level AAA) and WCAG 2.5.8 (Level AA, 2.2) require a minimum 24×24px target size with adequate spacing. Apple HIG and Material Design both recommend 44×44px as a practical minimum.
|
|
437
|
+
|
|
438
|
+
```css
|
|
439
|
+
.touch-target {
|
|
440
|
+
min-width: 44px;
|
|
441
|
+
min-height: 44px;
|
|
442
|
+
display: inline-flex;
|
|
443
|
+
align-items: center;
|
|
444
|
+
justify-content: center;
|
|
445
|
+
}
|
|
446
|
+
```
|
|
447
|
+
|
|
448
|
+
When the visual element must be smaller (e.g., a 16px icon), expand the hit area with padding or a pseudo-element.
|
|
449
|
+
|
|
450
|
+
```css
|
|
451
|
+
.icon-button {
|
|
452
|
+
position: relative;
|
|
453
|
+
width: 20px;
|
|
454
|
+
height: 20px;
|
|
455
|
+
}
|
|
456
|
+
.icon-button::before {
|
|
457
|
+
content: '';
|
|
458
|
+
position: absolute;
|
|
459
|
+
inset: -12px; /* extends 12px on each side → 44px total */
|
|
460
|
+
}
|
|
461
|
+
```
|
|
462
|
+
|
|
463
|
+
### Spacing Between Targets
|
|
464
|
+
|
|
465
|
+
Adjacent targets need at least 8px of gap so accidental activation is unlikely.
|
|
466
|
+
|
|
467
|
+
```css
|
|
468
|
+
.toolbar {
|
|
469
|
+
display: flex;
|
|
470
|
+
gap: 8px;
|
|
471
|
+
}
|
|
472
|
+
```
|
|
473
|
+
|
|
474
|
+
### DO / DON'T
|
|
475
|
+
|
|
476
|
+
- DO expand hit areas with padding or pseudo-elements rather than enlarging the visual element.
|
|
477
|
+
- DO maintain at least 8px spacing between adjacent interactive elements.
|
|
478
|
+
- DON'T place two destructive actions (e.g., "Delete" and "Archive") adjacent with minimal spacing.
|
|
479
|
+
- DON'T measure touch targets by their visual size — measure the actual interactive area.
|
|
480
|
+
|
|
481
|
+
---
|
|
482
|
+
|
|
483
|
+
## Loading Patterns
|
|
484
|
+
|
|
485
|
+
### Choosing the Right Pattern
|
|
486
|
+
|
|
487
|
+
Three loading patterns serve different situations.
|
|
488
|
+
|
|
489
|
+
**Skeleton screens** — use when you know the shape of the incoming content (a card, a table row, a list). They set accurate spatial expectations and feel faster because they show structure immediately.
|
|
490
|
+
|
|
491
|
+
**Spinners** — use when you do not know the shape of the result (a search, a file upload) or when the operation is fast enough (under 300ms) that a skeleton would flash.
|
|
492
|
+
|
|
493
|
+
**Progress bars** — use only when you have a real percentage to report (file upload, batch processing). A fake animated progress bar is worse than a spinner because it lies to the user.
|
|
494
|
+
|
|
495
|
+
```tsx
|
|
496
|
+
function SkeletonCard() {
|
|
497
|
+
return (
|
|
498
|
+
<div className="skeleton-card" aria-hidden="true">
|
|
499
|
+
<div className="skeleton skeleton--avatar" />
|
|
500
|
+
<div className="skeleton skeleton--line skeleton--line-80" />
|
|
501
|
+
<div className="skeleton skeleton--line skeleton--line-60" />
|
|
502
|
+
</div>
|
|
503
|
+
);
|
|
504
|
+
}
|
|
505
|
+
```
|
|
506
|
+
|
|
507
|
+
```css
|
|
508
|
+
@keyframes shimmer {
|
|
509
|
+
from { background-position: -200% 0; }
|
|
510
|
+
to { background-position: 200% 0; }
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
.skeleton {
|
|
514
|
+
background: linear-gradient(
|
|
515
|
+
90deg,
|
|
516
|
+
var(--color-skeleton-base) 25%,
|
|
517
|
+
var(--color-skeleton-highlight) 50%,
|
|
518
|
+
var(--color-skeleton-base) 75%
|
|
519
|
+
);
|
|
520
|
+
background-size: 200% 100%;
|
|
521
|
+
animation: shimmer 1.4s ease infinite;
|
|
522
|
+
border-radius: 4px;
|
|
523
|
+
}
|
|
524
|
+
```
|
|
525
|
+
|
|
526
|
+
### Announcing Loading State to Screen Readers
|
|
527
|
+
|
|
528
|
+
```tsx
|
|
529
|
+
function LoadingRegion({ isLoading, children }: { isLoading: boolean; children: React.ReactNode }) {
|
|
530
|
+
return (
|
|
531
|
+
<div aria-live="polite" aria-busy={isLoading}>
|
|
532
|
+
{isLoading ? <SkeletonCard /> : children}
|
|
533
|
+
</div>
|
|
534
|
+
);
|
|
535
|
+
}
|
|
536
|
+
```
|
|
537
|
+
|
|
538
|
+
### DO / DON'T
|
|
539
|
+
|
|
540
|
+
- DO use skeleton screens for content with a known layout.
|
|
541
|
+
- DO add `aria-busy="true"` and `aria-live="polite"` to regions that load asynchronously.
|
|
542
|
+
- DON'T use a fake animated progress bar that does not reflect real progress.
|
|
543
|
+
- DON'T show a spinner for operations that complete in under 100ms — the flash is disorienting.
|
|
544
|
+
- DON'T leave a spinner visible indefinitely without a timeout and an error fallback.
|