@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,591 +1,591 @@
|
|
|
1
|
-
# Motion Design Reference
|
|
2
|
-
|
|
3
|
-
Animation, transitions, and motion systems for production UI.
|
|
4
|
-
|
|
5
|
-
---
|
|
6
|
-
|
|
7
|
-
## Duration Rules
|
|
8
|
-
|
|
9
|
-
Duration is not aesthetic preference — it is physics. Elements that travel farther should take longer. Small, contained feedback should be instant. Full-page transitions should be deliberate.
|
|
10
|
-
|
|
11
|
-
### The Three Duration Bands
|
|
12
|
-
|
|
13
|
-
**Micro (100–150ms):** Immediate feedback for small, contained interactions. Button hover state, checkbox toggle, tooltip appear. The user's hand is on the mouse — any longer feels sluggish.
|
|
14
|
-
|
|
15
|
-
**Medium (200–300ms):** Standard UI transitions. Dropdowns opening, drawers sliding in, modals appearing, tab switching. This is the default duration for most interactions. 250ms is a reliable default if you have no other data.
|
|
16
|
-
|
|
17
|
-
**Large (400–500ms):** Full-screen transitions, onboarding flows, complex multi-part animations. Use sparingly. Every millisecond above 300ms requires justification — you are asking the user to wait.
|
|
18
|
-
|
|
19
|
-
```css
|
|
20
|
-
:root {
|
|
21
|
-
--duration-micro: 120ms;
|
|
22
|
-
--duration-medium: 250ms;
|
|
23
|
-
--duration-large: 450ms;
|
|
24
|
-
}
|
|
25
|
-
```
|
|
26
|
-
|
|
27
|
-
```tsx
|
|
28
|
-
// Tailwind duration classes mapped to the three bands
|
|
29
|
-
const DURATION = {
|
|
30
|
-
micro: 'duration-100', // 100ms
|
|
31
|
-
medium: 'duration-200', // 200ms (use 250 via arbitrary if needed)
|
|
32
|
-
large: 'duration-500', // 500ms
|
|
33
|
-
} as const;
|
|
34
|
-
```
|
|
35
|
-
|
|
36
|
-
### DO / DON'T
|
|
37
|
-
|
|
38
|
-
**DO** vary duration based on element size and travel distance. A small tooltip can fade in at 120ms. A full-page slide transition needs 400ms.
|
|
39
|
-
|
|
40
|
-
**DON'T** default every animation to `300ms` out of habit. Micro-interactions at 300ms accumulate into a UI that feels like it is wading through water.
|
|
41
|
-
|
|
42
|
-
```css
|
|
43
|
-
/* Bad — same duration for very different interactions */
|
|
44
|
-
.tooltip { transition: opacity 300ms; }
|
|
45
|
-
.full-page-modal { transition: transform 300ms; }
|
|
46
|
-
|
|
47
|
-
/* Good — duration proportional to scope */
|
|
48
|
-
.tooltip { transition: opacity var(--duration-micro); }
|
|
49
|
-
.full-page-modal { transition: transform var(--duration-large); }
|
|
50
|
-
```
|
|
51
|
-
|
|
52
|
-
**DON'T** animate beyond 500ms for any standard UI transition. If you need more time, question whether the transition is the right design decision.
|
|
53
|
-
|
|
54
|
-
---
|
|
55
|
-
|
|
56
|
-
## Easing Functions
|
|
57
|
-
|
|
58
|
-
Easing determines the velocity curve of an animation. Correct easing makes motion feel physically plausible. Incorrect easing — especially linear — makes motion feel mechanical and artificial.
|
|
59
|
-
|
|
60
|
-
### The Core Rule
|
|
61
|
-
|
|
62
|
-
- **Entering elements:** `ease-out` — fast start, slow finish. Simulates an object decelerating as it arrives.
|
|
63
|
-
- **Exiting elements:** `ease-in` — slow start, fast finish. Simulates an object accelerating as it leaves.
|
|
64
|
-
- **Moving elements:** `ease-in-out` — slow at both ends, fast in the middle. Natural for repositioning.
|
|
65
|
-
- **Never use `linear`** for transitions that simulate physical movement. Linear is correct only for progress bars, loading spinners, and explicitly mechanical animations.
|
|
66
|
-
|
|
67
|
-
### Cubic Bezier Reference
|
|
68
|
-
|
|
69
|
-
```css
|
|
70
|
-
:root {
|
|
71
|
-
/* Entering: decelerates to a stop */
|
|
72
|
-
--ease-out: cubic-bezier(0.0, 0.0, 0.2, 1.0);
|
|
73
|
-
|
|
74
|
-
/* Exiting: accelerates away */
|
|
75
|
-
--ease-in: cubic-bezier(0.4, 0.0, 1.0, 1.0);
|
|
76
|
-
|
|
77
|
-
/* Moving: ease in and out */
|
|
78
|
-
--ease-in-out: cubic-bezier(0.4, 0.0, 0.2, 1.0);
|
|
79
|
-
|
|
80
|
-
/* Spring-like overshoot (use sparingly) */
|
|
81
|
-
--ease-spring: cubic-bezier(0.34, 1.56, 0.64, 1.0);
|
|
82
|
-
}
|
|
83
|
-
```
|
|
84
|
-
|
|
85
|
-
The Material Design team validated these specific curves through extensive user testing. They are a safe default for any product.
|
|
86
|
-
|
|
87
|
-
### Applied in CSS
|
|
88
|
-
|
|
89
|
-
```css
|
|
90
|
-
/* Dropdown enters with ease-out, exits with ease-in */
|
|
91
|
-
.dropdown {
|
|
92
|
-
transform: translateY(-8px);
|
|
93
|
-
opacity: 0;
|
|
94
|
-
transition:
|
|
95
|
-
transform var(--duration-medium) var(--ease-out),
|
|
96
|
-
opacity var(--duration-medium) var(--ease-out);
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
.dropdown.open {
|
|
100
|
-
transform: translateY(0);
|
|
101
|
-
opacity: 1;
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
.dropdown.closing {
|
|
105
|
-
transition:
|
|
106
|
-
transform var(--duration-micro) var(--ease-in),
|
|
107
|
-
opacity var(--duration-micro) var(--ease-in);
|
|
108
|
-
}
|
|
109
|
-
```
|
|
110
|
-
|
|
111
|
-
### Applied in React with Framer Motion
|
|
112
|
-
|
|
113
|
-
```tsx
|
|
114
|
-
// Enter with ease-out, exit with ease-in
|
|
115
|
-
<motion.div
|
|
116
|
-
initial={{ opacity: 0, y: -8 }}
|
|
117
|
-
animate={{ opacity: 1, y: 0 }}
|
|
118
|
-
exit={{ opacity: 0, y: -8 }}
|
|
119
|
-
transition={{
|
|
120
|
-
duration: 0.25,
|
|
121
|
-
ease: [0.0, 0.0, 0.2, 1.0], // ease-out
|
|
122
|
-
}}
|
|
123
|
-
>
|
|
124
|
-
{children}
|
|
125
|
-
</motion.div>
|
|
126
|
-
```
|
|
127
|
-
|
|
128
|
-
### DO / DON'T
|
|
129
|
-
|
|
130
|
-
**DO** use different easing for enter and exit when both are visible. The asymmetry feels natural.
|
|
131
|
-
|
|
132
|
-
**DON'T** use `ease-out` for exits — it makes elements feel like they are fighting to leave.
|
|
133
|
-
|
|
134
|
-
```css
|
|
135
|
-
/* Bad — ease-out on exit feels wrong */
|
|
136
|
-
.modal-exit { transition: opacity 200ms ease-out; }
|
|
137
|
-
|
|
138
|
-
/* Good — ease-in on exit feels natural */
|
|
139
|
-
.modal-exit { transition: opacity 120ms var(--ease-in); }
|
|
140
|
-
```
|
|
141
|
-
|
|
142
|
-
---
|
|
143
|
-
|
|
144
|
-
## Stagger Patterns
|
|
145
|
-
|
|
146
|
-
Staggering cascades a timing delay across a list of elements so they animate sequentially rather than simultaneously. A simultaneous animation of 20 cards is visually noisy. The same cards staggered at 60ms intervals read as a clean, organized reveal.
|
|
147
|
-
|
|
148
|
-
### The Formula
|
|
149
|
-
|
|
150
|
-
```
|
|
151
|
-
elementDelay = index * staggerInterval
|
|
152
|
-
```
|
|
153
|
-
|
|
154
|
-
A stagger interval of 50–100ms is the reliable range. Below 40ms the stagger is imperceptible. Above 120ms the animation drags and the last element appears too late.
|
|
155
|
-
|
|
156
|
-
### CSS Custom Properties Approach
|
|
157
|
-
|
|
158
|
-
```css
|
|
159
|
-
.card {
|
|
160
|
-
opacity: 0;
|
|
161
|
-
transform: translateY(12px);
|
|
162
|
-
animation: card-enter var(--duration-medium) var(--ease-out) forwards;
|
|
163
|
-
animation-delay: calc(var(--index) * 60ms);
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
@keyframes card-enter {
|
|
167
|
-
to {
|
|
168
|
-
opacity: 1;
|
|
169
|
-
transform: translateY(0);
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
```
|
|
173
|
-
|
|
174
|
-
```tsx
|
|
175
|
-
// Set the CSS custom property per element
|
|
176
|
-
{cards.map((card, index) => (
|
|
177
|
-
<div
|
|
178
|
-
key={card.id}
|
|
179
|
-
className="card"
|
|
180
|
-
style={{ '--index': index } as React.CSSProperties}
|
|
181
|
-
>
|
|
182
|
-
<Card {...card} />
|
|
183
|
-
</div>
|
|
184
|
-
))}
|
|
185
|
-
```
|
|
186
|
-
|
|
187
|
-
### React with Framer Motion
|
|
188
|
-
|
|
189
|
-
```tsx
|
|
190
|
-
const container = {
|
|
191
|
-
hidden: {},
|
|
192
|
-
show: {
|
|
193
|
-
transition: {
|
|
194
|
-
staggerChildren: 0.06, // 60ms between each child
|
|
195
|
-
},
|
|
196
|
-
},
|
|
197
|
-
};
|
|
198
|
-
|
|
199
|
-
const item = {
|
|
200
|
-
hidden: { opacity: 0, y: 12 },
|
|
201
|
-
show: {
|
|
202
|
-
opacity: 1,
|
|
203
|
-
y: 0,
|
|
204
|
-
transition: { duration: 0.25, ease: [0.0, 0.0, 0.2, 1.0] },
|
|
205
|
-
},
|
|
206
|
-
};
|
|
207
|
-
|
|
208
|
-
function CardGrid({ cards }: { cards: Card[] }) {
|
|
209
|
-
return (
|
|
210
|
-
<motion.div
|
|
211
|
-
className="grid gap-6"
|
|
212
|
-
style={{ gridTemplateColumns: 'repeat(auto-fit, minmax(280px, 1fr))' }}
|
|
213
|
-
variants={container}
|
|
214
|
-
initial="hidden"
|
|
215
|
-
animate="show"
|
|
216
|
-
>
|
|
217
|
-
{cards.map(card => (
|
|
218
|
-
<motion.div key={card.id} variants={item}>
|
|
219
|
-
<Card {...card} />
|
|
220
|
-
</motion.div>
|
|
221
|
-
))}
|
|
222
|
-
</motion.div>
|
|
223
|
-
);
|
|
224
|
-
}
|
|
225
|
-
```
|
|
226
|
-
|
|
227
|
-
### DO / DON'T
|
|
228
|
-
|
|
229
|
-
**DO** cap your stagger list. Staggering 50 items at 60ms means the last item starts animating 3 seconds after the first. Cap at 15–20 items maximum, or reduce the interval proportionally.
|
|
230
|
-
|
|
231
|
-
```tsx
|
|
232
|
-
const staggerInterval = Math.min(60, 800 / cards.length); // never exceed 800ms total
|
|
233
|
-
```
|
|
234
|
-
|
|
235
|
-
**DON'T** stagger UI that appears in response to a direct user interaction (clicking a button). Stagger is for reveals — content that loads or enters as a set. Staggering a button's icon and label on click is disorienting.
|
|
236
|
-
|
|
237
|
-
---
|
|
238
|
-
|
|
239
|
-
## GPU Acceleration
|
|
240
|
-
|
|
241
|
-
The browser's compositor thread handles `transform` and `opacity` changes without involving the main thread. Every other CSS property — `width`, `height`, `top`, `left`, `background-color`, `border-radius`, `box-shadow` — triggers layout or paint recalculation on the main thread and produces jank.
|
|
242
|
-
|
|
243
|
-
### The Rule
|
|
244
|
-
|
|
245
|
-
**Only animate `transform` and `opacity`.** Everything else is off the table for animations.
|
|
246
|
-
|
|
247
|
-
```css
|
|
248
|
-
/* Bad — animating width triggers layout recalculation on every frame */
|
|
249
|
-
.panel {
|
|
250
|
-
width: 0;
|
|
251
|
-
transition: width 300ms ease-out;
|
|
252
|
-
}
|
|
253
|
-
.panel.open { width: 320px; }
|
|
254
|
-
|
|
255
|
-
/* Good — transform doesn't trigger layout */
|
|
256
|
-
.panel {
|
|
257
|
-
transform: translateX(-320px);
|
|
258
|
-
transition: transform 300ms var(--ease-out);
|
|
259
|
-
}
|
|
260
|
-
.panel.open { transform: translateX(0); }
|
|
261
|
-
```
|
|
262
|
-
|
|
263
|
-
### Simulating Non-Transform Properties
|
|
264
|
-
|
|
265
|
-
| What you want | How to do it with transform/opacity |
|
|
266
|
-
|---|---|
|
|
267
|
-
| Width expand | `scaleX()` + `transform-origin: left` |
|
|
268
|
-
| Height expand | `scaleY()` + `transform-origin: top` |
|
|
269
|
-
| Slide in from left | `translateX(-100%)` → `translateX(0)` |
|
|
270
|
-
| Fade background | `opacity` on an overlay element |
|
|
271
|
-
| Grow a button | `scale(0.95)` → `scale(1)` |
|
|
272
|
-
|
|
273
|
-
```css
|
|
274
|
-
/* Simulating height expand without animating height */
|
|
275
|
-
.accordion-content {
|
|
276
|
-
transform: scaleY(0);
|
|
277
|
-
transform-origin: top;
|
|
278
|
-
opacity: 0;
|
|
279
|
-
transition:
|
|
280
|
-
transform var(--duration-medium) var(--ease-out),
|
|
281
|
-
opacity var(--duration-medium) var(--ease-out);
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
.accordion-content.open {
|
|
285
|
-
transform: scaleY(1);
|
|
286
|
-
opacity: 1;
|
|
287
|
-
}
|
|
288
|
-
```
|
|
289
|
-
|
|
290
|
-
### `will-change`
|
|
291
|
-
|
|
292
|
-
`will-change: transform` hints to the browser to promote an element to its own compositor layer before animation begins, eliminating the ramp-up cost.
|
|
293
|
-
|
|
294
|
-
```css
|
|
295
|
-
/* Only use will-change on elements that animate frequently */
|
|
296
|
-
.animated-element {
|
|
297
|
-
will-change: transform, opacity;
|
|
298
|
-
}
|
|
299
|
-
```
|
|
300
|
-
|
|
301
|
-
**Caution:** `will-change` consumes GPU memory. Apply it only to elements that animate. Do not apply it to static elements or as a blanket performance fix.
|
|
302
|
-
|
|
303
|
-
### DO / DON'T
|
|
304
|
-
|
|
305
|
-
**DO** test animations at 4x CPU throttle in Chrome DevTools. If frames drop below 60fps on throttled CPU, the animation is too expensive.
|
|
306
|
-
|
|
307
|
-
**DON'T** animate `box-shadow` directly — it is extremely expensive. Instead, animate `opacity` on a pseudo-element that has the shadow.
|
|
308
|
-
|
|
309
|
-
```css
|
|
310
|
-
/* Bad — repaints on every frame */
|
|
311
|
-
.card { transition: box-shadow 200ms ease-out; }
|
|
312
|
-
.card:hover { box-shadow: 0 20px 40px rgba(0,0,0,0.2); }
|
|
313
|
-
|
|
314
|
-
/* Good — opacity change is GPU composited */
|
|
315
|
-
.card { position: relative; }
|
|
316
|
-
.card::after {
|
|
317
|
-
content: '';
|
|
318
|
-
position: absolute;
|
|
319
|
-
inset: 0;
|
|
320
|
-
border-radius: inherit;
|
|
321
|
-
box-shadow: 0 20px 40px rgba(0,0,0,0.2);
|
|
322
|
-
opacity: 0;
|
|
323
|
-
transition: opacity 200ms ease-out;
|
|
324
|
-
}
|
|
325
|
-
.card:hover::after { opacity: 1; }
|
|
326
|
-
```
|
|
327
|
-
|
|
328
|
-
---
|
|
329
|
-
|
|
330
|
-
## Reduced Motion
|
|
331
|
-
|
|
332
|
-
The `prefers-reduced-motion` media query is not optional — it is a mandatory accessibility requirement. Users who set this preference include people with vestibular disorders (motion sickness from animation), epilepsy risk, and cognitive disabilities where motion creates distraction.
|
|
333
|
-
|
|
334
|
-
### The Two Categories
|
|
335
|
-
|
|
336
|
-
**Decorative motion** (remove entirely): Entrance animations, hover effects, parallax, background animations, staggered reveals. These serve aesthetic purposes only. Disable them completely.
|
|
337
|
-
|
|
338
|
-
**Functional motion** (keep, but simplify): Toast notifications sliding in, modal appearing to indicate focus shift, loading spinners. These communicate state — remove them and the user loses information. Replace movement with instant opacity transitions instead.
|
|
339
|
-
|
|
340
|
-
```css
|
|
341
|
-
@media (prefers-reduced-motion: reduce) {
|
|
342
|
-
/* Remove all decorative animations */
|
|
343
|
-
.card-enter,
|
|
344
|
-
.page-transition,
|
|
345
|
-
.stagger-item {
|
|
346
|
-
animation: none;
|
|
347
|
-
transition: none;
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
/* Simplify functional animations to opacity only */
|
|
351
|
-
.modal {
|
|
352
|
-
transition: opacity var(--duration-micro) linear;
|
|
353
|
-
/* No transform — just fade */
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
.toast {
|
|
357
|
-
transition: opacity var(--duration-micro) linear;
|
|
358
|
-
/* Remove translateY slide */
|
|
359
|
-
}
|
|
360
|
-
}
|
|
361
|
-
```
|
|
362
|
-
|
|
363
|
-
### Global Reset Pattern
|
|
364
|
-
|
|
365
|
-
```css
|
|
366
|
-
/* Apply to all elements when user prefers reduced motion */
|
|
367
|
-
@media (prefers-reduced-motion: reduce) {
|
|
368
|
-
*,
|
|
369
|
-
*::before,
|
|
370
|
-
*::after {
|
|
371
|
-
animation-duration: 0.01ms !important;
|
|
372
|
-
animation-iteration-count: 1 !important;
|
|
373
|
-
transition-duration: 0.01ms !important;
|
|
374
|
-
scroll-behavior: auto !important;
|
|
375
|
-
}
|
|
376
|
-
}
|
|
377
|
-
```
|
|
378
|
-
|
|
379
|
-
### React Hook
|
|
380
|
-
|
|
381
|
-
```tsx
|
|
382
|
-
function useReducedMotion(): boolean {
|
|
383
|
-
const [prefersReduced, setPrefersReduced] = React.useState<boolean>(() => {
|
|
384
|
-
if (typeof window === 'undefined') return false;
|
|
385
|
-
return window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
|
386
|
-
});
|
|
387
|
-
|
|
388
|
-
React.useEffect(() => {
|
|
389
|
-
const mediaQuery = window.matchMedia('(prefers-reduced-motion: reduce)');
|
|
390
|
-
const handler = (e: MediaQueryListEvent): void => setPrefersReduced(e.matches);
|
|
391
|
-
mediaQuery.addEventListener('change', handler);
|
|
392
|
-
return () => mediaQuery.removeEventListener('change', handler);
|
|
393
|
-
}, []);
|
|
394
|
-
|
|
395
|
-
return prefersReduced;
|
|
396
|
-
}
|
|
397
|
-
```
|
|
398
|
-
|
|
399
|
-
```tsx
|
|
400
|
-
function AnimatedCard({ children }: { children: React.ReactNode }) {
|
|
401
|
-
const reducedMotion = useReducedMotion();
|
|
402
|
-
|
|
403
|
-
return (
|
|
404
|
-
<motion.div
|
|
405
|
-
initial={reducedMotion ? false : { opacity: 0, y: 12 }}
|
|
406
|
-
animate={{ opacity: 1, y: 0 }}
|
|
407
|
-
transition={reducedMotion ? { duration: 0 } : { duration: 0.25 }}
|
|
408
|
-
>
|
|
409
|
-
{children}
|
|
410
|
-
</motion.div>
|
|
411
|
-
);
|
|
412
|
-
}
|
|
413
|
-
```
|
|
414
|
-
|
|
415
|
-
### DO / DON'T
|
|
416
|
-
|
|
417
|
-
**DO** test your UI with `prefers-reduced-motion: reduce` enabled in OS settings before every release.
|
|
418
|
-
|
|
419
|
-
**DON'T** use `prefers-reduced-motion` as an afterthought or accessibility checkbox. Build it into your animation system from the start so it applies automatically.
|
|
420
|
-
|
|
421
|
-
---
|
|
422
|
-
|
|
423
|
-
## Perceived Performance
|
|
424
|
-
|
|
425
|
-
Perceived performance is how fast your UI feels, independent of how fast it actually is. A 2-second load that shows content progressively feels faster than a 1-second load that shows nothing then dumps everything at once.
|
|
426
|
-
|
|
427
|
-
### Skeleton Screens
|
|
428
|
-
|
|
429
|
-
Replace empty loading states with skeletons that match the layout of the real content. The user understands what is loading and can orient their attention.
|
|
430
|
-
|
|
431
|
-
```tsx
|
|
432
|
-
function CardSkeleton() {
|
|
433
|
-
return (
|
|
434
|
-
<div className="p-6 rounded-xl border border-zinc-200 space-y-3 animate-pulse">
|
|
435
|
-
<div className="h-4 w-3/4 bg-zinc-200 rounded" />
|
|
436
|
-
<div className="h-3 w-full bg-zinc-100 rounded" />
|
|
437
|
-
<div className="h-3 w-2/3 bg-zinc-100 rounded" />
|
|
438
|
-
<div className="h-8 w-24 bg-zinc-200 rounded-lg mt-4" />
|
|
439
|
-
</div>
|
|
440
|
-
);
|
|
441
|
-
}
|
|
442
|
-
|
|
443
|
-
function CardList({ isLoading, cards }: { isLoading: boolean; cards: Card[] }) {
|
|
444
|
-
if (isLoading) {
|
|
445
|
-
return (
|
|
446
|
-
<div className="grid gap-6" style={{ gridTemplateColumns: 'repeat(auto-fit, minmax(280px, 1fr))' }}>
|
|
447
|
-
{Array.from({ length: 6 }).map((_, i) => <CardSkeleton key={i} />)}
|
|
448
|
-
</div>
|
|
449
|
-
);
|
|
450
|
-
}
|
|
451
|
-
return (
|
|
452
|
-
<div className="grid gap-6" style={{ gridTemplateColumns: 'repeat(auto-fit, minmax(280px, 1fr))' }}>
|
|
453
|
-
{cards.map(card => <Card key={card.id} {...card} />)}
|
|
454
|
-
</div>
|
|
455
|
-
);
|
|
456
|
-
}
|
|
457
|
-
```
|
|
458
|
-
|
|
459
|
-
### Optimistic UI
|
|
460
|
-
|
|
461
|
-
For actions with high confidence of success (creating a note, liking a post, toggling a setting), update the UI immediately and reconcile with the server response asynchronously. If the server returns an error, roll back with a toast notification.
|
|
462
|
-
|
|
463
|
-
```tsx
|
|
464
|
-
function useLike(postId: string) {
|
|
465
|
-
const [liked, setLiked] = React.useState(false);
|
|
466
|
-
|
|
467
|
-
const toggleLike = async (): Promise<void> => {
|
|
468
|
-
const previous = liked;
|
|
469
|
-
setLiked(!liked); // optimistic update — instant feedback
|
|
470
|
-
try {
|
|
471
|
-
await api.toggleLike(postId);
|
|
472
|
-
} catch {
|
|
473
|
-
setLiked(previous); // rollback on error
|
|
474
|
-
toast.error('Could not update like — try again');
|
|
475
|
-
}
|
|
476
|
-
};
|
|
477
|
-
|
|
478
|
-
return { liked, toggleLike };
|
|
479
|
-
}
|
|
480
|
-
```
|
|
481
|
-
|
|
482
|
-
---
|
|
483
|
-
|
|
484
|
-
## Scroll-Driven Animations
|
|
485
|
-
|
|
486
|
-
The CSS Scroll-Driven Animations API links animation progress directly to scroll position without JavaScript. It runs entirely on the compositor thread — no `scroll` event listeners, no `requestAnimationFrame`, no main thread cost.
|
|
487
|
-
|
|
488
|
-
### `animation-timeline: scroll()`
|
|
489
|
-
|
|
490
|
-
`scroll()` links animation progress to the scroll position of a scroll container.
|
|
491
|
-
|
|
492
|
-
```css
|
|
493
|
-
/* Progress bar that fills as the user scrolls the page */
|
|
494
|
-
.scroll-progress {
|
|
495
|
-
position: fixed;
|
|
496
|
-
top: 0;
|
|
497
|
-
left: 0;
|
|
498
|
-
height: 3px;
|
|
499
|
-
width: 100%;
|
|
500
|
-
background: linear-gradient(to right, #3b82f6, #8b5cf6);
|
|
501
|
-
transform-origin: left;
|
|
502
|
-
animation: progress-grow linear;
|
|
503
|
-
animation-timeline: scroll(root block);
|
|
504
|
-
}
|
|
505
|
-
|
|
506
|
-
@keyframes progress-grow {
|
|
507
|
-
from { transform: scaleX(0); }
|
|
508
|
-
to { transform: scaleX(1); }
|
|
509
|
-
}
|
|
510
|
-
```
|
|
511
|
-
|
|
512
|
-
### `animation-timeline: view()`
|
|
513
|
-
|
|
514
|
-
`view()` links animation progress to how much of an element is visible in the viewport. This is the correct replacement for IntersectionObserver reveal patterns.
|
|
515
|
-
|
|
516
|
-
```css
|
|
517
|
-
/* Element fades and translates in as it enters the viewport */
|
|
518
|
-
.reveal-on-scroll {
|
|
519
|
-
animation: reveal-item linear both;
|
|
520
|
-
animation-timeline: view();
|
|
521
|
-
animation-range: entry 0% entry 30%;
|
|
522
|
-
}
|
|
523
|
-
|
|
524
|
-
@keyframes reveal-item {
|
|
525
|
-
from {
|
|
526
|
-
opacity: 0;
|
|
527
|
-
transform: translateY(24px);
|
|
528
|
-
}
|
|
529
|
-
to {
|
|
530
|
-
opacity: 1;
|
|
531
|
-
transform: translateY(0);
|
|
532
|
-
}
|
|
533
|
-
}
|
|
534
|
-
```
|
|
535
|
-
|
|
536
|
-
The `animation-range: entry 0% entry 30%` clause means the animation runs from when the element starts entering the viewport to when 30% of it is visible.
|
|
537
|
-
|
|
538
|
-
### React Integration
|
|
539
|
-
|
|
540
|
-
```tsx
|
|
541
|
-
// No JavaScript needed — pure CSS
|
|
542
|
-
// Apply the class and the browser handles the rest
|
|
543
|
-
|
|
544
|
-
function RevealSection({ children }: { children: React.ReactNode }) {
|
|
545
|
-
return (
|
|
546
|
-
<section className="reveal-on-scroll py-16">
|
|
547
|
-
{children}
|
|
548
|
-
</section>
|
|
549
|
-
);
|
|
550
|
-
}
|
|
551
|
-
```
|
|
552
|
-
|
|
553
|
-
### Browser Support and Fallback
|
|
554
|
-
|
|
555
|
-
Scroll-driven animations are supported in Chrome 115+ and Safari 18+. Firefox support is in progress. Always include a fallback:
|
|
556
|
-
|
|
557
|
-
```css
|
|
558
|
-
/* Base state — visible by default (fallback for unsupported browsers) */
|
|
559
|
-
.reveal-on-scroll {
|
|
560
|
-
opacity: 1;
|
|
561
|
-
transform: none;
|
|
562
|
-
}
|
|
563
|
-
|
|
564
|
-
/* Enhanced — scroll-driven animation for supported browsers */
|
|
565
|
-
@supports (animation-timeline: scroll()) {
|
|
566
|
-
.reveal-on-scroll {
|
|
567
|
-
opacity: 0;
|
|
568
|
-
transform: translateY(24px);
|
|
569
|
-
animation: reveal-item linear both;
|
|
570
|
-
animation-timeline: view();
|
|
571
|
-
animation-range: entry 0% entry 30%;
|
|
572
|
-
}
|
|
573
|
-
}
|
|
574
|
-
|
|
575
|
-
/* Always respect reduced motion */
|
|
576
|
-
@media (prefers-reduced-motion: reduce) {
|
|
577
|
-
.reveal-on-scroll {
|
|
578
|
-
animation: none !important;
|
|
579
|
-
opacity: 1 !important;
|
|
580
|
-
transform: none !important;
|
|
581
|
-
}
|
|
582
|
-
}
|
|
583
|
-
```
|
|
584
|
-
|
|
585
|
-
### DO / DON'T
|
|
586
|
-
|
|
587
|
-
**DO** use scroll-driven animations over JavaScript IntersectionObserver for simple reveal effects. The compositor-thread execution eliminates scroll jank.
|
|
588
|
-
|
|
589
|
-
**DON'T** use scroll-driven animations for complex stateful interactions (shopping cart updates, form validation, navigation). Those require JavaScript logic that cannot be expressed in CSS keyframes.
|
|
590
|
-
|
|
591
|
-
**DO** always pair `@supports (animation-timeline: scroll())` with a visible fallback state — unsupported browsers must not show hidden or broken content.
|
|
1
|
+
# Motion Design Reference
|
|
2
|
+
|
|
3
|
+
Animation, transitions, and motion systems for production UI.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Duration Rules
|
|
8
|
+
|
|
9
|
+
Duration is not aesthetic preference — it is physics. Elements that travel farther should take longer. Small, contained feedback should be instant. Full-page transitions should be deliberate.
|
|
10
|
+
|
|
11
|
+
### The Three Duration Bands
|
|
12
|
+
|
|
13
|
+
**Micro (100–150ms):** Immediate feedback for small, contained interactions. Button hover state, checkbox toggle, tooltip appear. The user's hand is on the mouse — any longer feels sluggish.
|
|
14
|
+
|
|
15
|
+
**Medium (200–300ms):** Standard UI transitions. Dropdowns opening, drawers sliding in, modals appearing, tab switching. This is the default duration for most interactions. 250ms is a reliable default if you have no other data.
|
|
16
|
+
|
|
17
|
+
**Large (400–500ms):** Full-screen transitions, onboarding flows, complex multi-part animations. Use sparingly. Every millisecond above 300ms requires justification — you are asking the user to wait.
|
|
18
|
+
|
|
19
|
+
```css
|
|
20
|
+
:root {
|
|
21
|
+
--duration-micro: 120ms;
|
|
22
|
+
--duration-medium: 250ms;
|
|
23
|
+
--duration-large: 450ms;
|
|
24
|
+
}
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
```tsx
|
|
28
|
+
// Tailwind duration classes mapped to the three bands
|
|
29
|
+
const DURATION = {
|
|
30
|
+
micro: 'duration-100', // 100ms
|
|
31
|
+
medium: 'duration-200', // 200ms (use 250 via arbitrary if needed)
|
|
32
|
+
large: 'duration-500', // 500ms
|
|
33
|
+
} as const;
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
### DO / DON'T
|
|
37
|
+
|
|
38
|
+
**DO** vary duration based on element size and travel distance. A small tooltip can fade in at 120ms. A full-page slide transition needs 400ms.
|
|
39
|
+
|
|
40
|
+
**DON'T** default every animation to `300ms` out of habit. Micro-interactions at 300ms accumulate into a UI that feels like it is wading through water.
|
|
41
|
+
|
|
42
|
+
```css
|
|
43
|
+
/* Bad — same duration for very different interactions */
|
|
44
|
+
.tooltip { transition: opacity 300ms; }
|
|
45
|
+
.full-page-modal { transition: transform 300ms; }
|
|
46
|
+
|
|
47
|
+
/* Good — duration proportional to scope */
|
|
48
|
+
.tooltip { transition: opacity var(--duration-micro); }
|
|
49
|
+
.full-page-modal { transition: transform var(--duration-large); }
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
**DON'T** animate beyond 500ms for any standard UI transition. If you need more time, question whether the transition is the right design decision.
|
|
53
|
+
|
|
54
|
+
---
|
|
55
|
+
|
|
56
|
+
## Easing Functions
|
|
57
|
+
|
|
58
|
+
Easing determines the velocity curve of an animation. Correct easing makes motion feel physically plausible. Incorrect easing — especially linear — makes motion feel mechanical and artificial.
|
|
59
|
+
|
|
60
|
+
### The Core Rule
|
|
61
|
+
|
|
62
|
+
- **Entering elements:** `ease-out` — fast start, slow finish. Simulates an object decelerating as it arrives.
|
|
63
|
+
- **Exiting elements:** `ease-in` — slow start, fast finish. Simulates an object accelerating as it leaves.
|
|
64
|
+
- **Moving elements:** `ease-in-out` — slow at both ends, fast in the middle. Natural for repositioning.
|
|
65
|
+
- **Never use `linear`** for transitions that simulate physical movement. Linear is correct only for progress bars, loading spinners, and explicitly mechanical animations.
|
|
66
|
+
|
|
67
|
+
### Cubic Bezier Reference
|
|
68
|
+
|
|
69
|
+
```css
|
|
70
|
+
:root {
|
|
71
|
+
/* Entering: decelerates to a stop */
|
|
72
|
+
--ease-out: cubic-bezier(0.0, 0.0, 0.2, 1.0);
|
|
73
|
+
|
|
74
|
+
/* Exiting: accelerates away */
|
|
75
|
+
--ease-in: cubic-bezier(0.4, 0.0, 1.0, 1.0);
|
|
76
|
+
|
|
77
|
+
/* Moving: ease in and out */
|
|
78
|
+
--ease-in-out: cubic-bezier(0.4, 0.0, 0.2, 1.0);
|
|
79
|
+
|
|
80
|
+
/* Spring-like overshoot (use sparingly) */
|
|
81
|
+
--ease-spring: cubic-bezier(0.34, 1.56, 0.64, 1.0);
|
|
82
|
+
}
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
The Material Design team validated these specific curves through extensive user testing. They are a safe default for any product.
|
|
86
|
+
|
|
87
|
+
### Applied in CSS
|
|
88
|
+
|
|
89
|
+
```css
|
|
90
|
+
/* Dropdown enters with ease-out, exits with ease-in */
|
|
91
|
+
.dropdown {
|
|
92
|
+
transform: translateY(-8px);
|
|
93
|
+
opacity: 0;
|
|
94
|
+
transition:
|
|
95
|
+
transform var(--duration-medium) var(--ease-out),
|
|
96
|
+
opacity var(--duration-medium) var(--ease-out);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
.dropdown.open {
|
|
100
|
+
transform: translateY(0);
|
|
101
|
+
opacity: 1;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
.dropdown.closing {
|
|
105
|
+
transition:
|
|
106
|
+
transform var(--duration-micro) var(--ease-in),
|
|
107
|
+
opacity var(--duration-micro) var(--ease-in);
|
|
108
|
+
}
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
### Applied in React with Framer Motion
|
|
112
|
+
|
|
113
|
+
```tsx
|
|
114
|
+
// Enter with ease-out, exit with ease-in
|
|
115
|
+
<motion.div
|
|
116
|
+
initial={{ opacity: 0, y: -8 }}
|
|
117
|
+
animate={{ opacity: 1, y: 0 }}
|
|
118
|
+
exit={{ opacity: 0, y: -8 }}
|
|
119
|
+
transition={{
|
|
120
|
+
duration: 0.25,
|
|
121
|
+
ease: [0.0, 0.0, 0.2, 1.0], // ease-out
|
|
122
|
+
}}
|
|
123
|
+
>
|
|
124
|
+
{children}
|
|
125
|
+
</motion.div>
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
### DO / DON'T
|
|
129
|
+
|
|
130
|
+
**DO** use different easing for enter and exit when both are visible. The asymmetry feels natural.
|
|
131
|
+
|
|
132
|
+
**DON'T** use `ease-out` for exits — it makes elements feel like they are fighting to leave.
|
|
133
|
+
|
|
134
|
+
```css
|
|
135
|
+
/* Bad — ease-out on exit feels wrong */
|
|
136
|
+
.modal-exit { transition: opacity 200ms ease-out; }
|
|
137
|
+
|
|
138
|
+
/* Good — ease-in on exit feels natural */
|
|
139
|
+
.modal-exit { transition: opacity 120ms var(--ease-in); }
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
---
|
|
143
|
+
|
|
144
|
+
## Stagger Patterns
|
|
145
|
+
|
|
146
|
+
Staggering cascades a timing delay across a list of elements so they animate sequentially rather than simultaneously. A simultaneous animation of 20 cards is visually noisy. The same cards staggered at 60ms intervals read as a clean, organized reveal.
|
|
147
|
+
|
|
148
|
+
### The Formula
|
|
149
|
+
|
|
150
|
+
```
|
|
151
|
+
elementDelay = index * staggerInterval
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
A stagger interval of 50–100ms is the reliable range. Below 40ms the stagger is imperceptible. Above 120ms the animation drags and the last element appears too late.
|
|
155
|
+
|
|
156
|
+
### CSS Custom Properties Approach
|
|
157
|
+
|
|
158
|
+
```css
|
|
159
|
+
.card {
|
|
160
|
+
opacity: 0;
|
|
161
|
+
transform: translateY(12px);
|
|
162
|
+
animation: card-enter var(--duration-medium) var(--ease-out) forwards;
|
|
163
|
+
animation-delay: calc(var(--index) * 60ms);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
@keyframes card-enter {
|
|
167
|
+
to {
|
|
168
|
+
opacity: 1;
|
|
169
|
+
transform: translateY(0);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
```tsx
|
|
175
|
+
// Set the CSS custom property per element
|
|
176
|
+
{cards.map((card, index) => (
|
|
177
|
+
<div
|
|
178
|
+
key={card.id}
|
|
179
|
+
className="card"
|
|
180
|
+
style={{ '--index': index } as React.CSSProperties}
|
|
181
|
+
>
|
|
182
|
+
<Card {...card} />
|
|
183
|
+
</div>
|
|
184
|
+
))}
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
### React with Framer Motion
|
|
188
|
+
|
|
189
|
+
```tsx
|
|
190
|
+
const container = {
|
|
191
|
+
hidden: {},
|
|
192
|
+
show: {
|
|
193
|
+
transition: {
|
|
194
|
+
staggerChildren: 0.06, // 60ms between each child
|
|
195
|
+
},
|
|
196
|
+
},
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
const item = {
|
|
200
|
+
hidden: { opacity: 0, y: 12 },
|
|
201
|
+
show: {
|
|
202
|
+
opacity: 1,
|
|
203
|
+
y: 0,
|
|
204
|
+
transition: { duration: 0.25, ease: [0.0, 0.0, 0.2, 1.0] },
|
|
205
|
+
},
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
function CardGrid({ cards }: { cards: Card[] }) {
|
|
209
|
+
return (
|
|
210
|
+
<motion.div
|
|
211
|
+
className="grid gap-6"
|
|
212
|
+
style={{ gridTemplateColumns: 'repeat(auto-fit, minmax(280px, 1fr))' }}
|
|
213
|
+
variants={container}
|
|
214
|
+
initial="hidden"
|
|
215
|
+
animate="show"
|
|
216
|
+
>
|
|
217
|
+
{cards.map(card => (
|
|
218
|
+
<motion.div key={card.id} variants={item}>
|
|
219
|
+
<Card {...card} />
|
|
220
|
+
</motion.div>
|
|
221
|
+
))}
|
|
222
|
+
</motion.div>
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
### DO / DON'T
|
|
228
|
+
|
|
229
|
+
**DO** cap your stagger list. Staggering 50 items at 60ms means the last item starts animating 3 seconds after the first. Cap at 15–20 items maximum, or reduce the interval proportionally.
|
|
230
|
+
|
|
231
|
+
```tsx
|
|
232
|
+
const staggerInterval = Math.min(60, 800 / cards.length); // never exceed 800ms total
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
**DON'T** stagger UI that appears in response to a direct user interaction (clicking a button). Stagger is for reveals — content that loads or enters as a set. Staggering a button's icon and label on click is disorienting.
|
|
236
|
+
|
|
237
|
+
---
|
|
238
|
+
|
|
239
|
+
## GPU Acceleration
|
|
240
|
+
|
|
241
|
+
The browser's compositor thread handles `transform` and `opacity` changes without involving the main thread. Every other CSS property — `width`, `height`, `top`, `left`, `background-color`, `border-radius`, `box-shadow` — triggers layout or paint recalculation on the main thread and produces jank.
|
|
242
|
+
|
|
243
|
+
### The Rule
|
|
244
|
+
|
|
245
|
+
**Only animate `transform` and `opacity`.** Everything else is off the table for animations.
|
|
246
|
+
|
|
247
|
+
```css
|
|
248
|
+
/* Bad — animating width triggers layout recalculation on every frame */
|
|
249
|
+
.panel {
|
|
250
|
+
width: 0;
|
|
251
|
+
transition: width 300ms ease-out;
|
|
252
|
+
}
|
|
253
|
+
.panel.open { width: 320px; }
|
|
254
|
+
|
|
255
|
+
/* Good — transform doesn't trigger layout */
|
|
256
|
+
.panel {
|
|
257
|
+
transform: translateX(-320px);
|
|
258
|
+
transition: transform 300ms var(--ease-out);
|
|
259
|
+
}
|
|
260
|
+
.panel.open { transform: translateX(0); }
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
### Simulating Non-Transform Properties
|
|
264
|
+
|
|
265
|
+
| What you want | How to do it with transform/opacity |
|
|
266
|
+
|---|---|
|
|
267
|
+
| Width expand | `scaleX()` + `transform-origin: left` |
|
|
268
|
+
| Height expand | `scaleY()` + `transform-origin: top` |
|
|
269
|
+
| Slide in from left | `translateX(-100%)` → `translateX(0)` |
|
|
270
|
+
| Fade background | `opacity` on an overlay element |
|
|
271
|
+
| Grow a button | `scale(0.95)` → `scale(1)` |
|
|
272
|
+
|
|
273
|
+
```css
|
|
274
|
+
/* Simulating height expand without animating height */
|
|
275
|
+
.accordion-content {
|
|
276
|
+
transform: scaleY(0);
|
|
277
|
+
transform-origin: top;
|
|
278
|
+
opacity: 0;
|
|
279
|
+
transition:
|
|
280
|
+
transform var(--duration-medium) var(--ease-out),
|
|
281
|
+
opacity var(--duration-medium) var(--ease-out);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
.accordion-content.open {
|
|
285
|
+
transform: scaleY(1);
|
|
286
|
+
opacity: 1;
|
|
287
|
+
}
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
### `will-change`
|
|
291
|
+
|
|
292
|
+
`will-change: transform` hints to the browser to promote an element to its own compositor layer before animation begins, eliminating the ramp-up cost.
|
|
293
|
+
|
|
294
|
+
```css
|
|
295
|
+
/* Only use will-change on elements that animate frequently */
|
|
296
|
+
.animated-element {
|
|
297
|
+
will-change: transform, opacity;
|
|
298
|
+
}
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
**Caution:** `will-change` consumes GPU memory. Apply it only to elements that animate. Do not apply it to static elements or as a blanket performance fix.
|
|
302
|
+
|
|
303
|
+
### DO / DON'T
|
|
304
|
+
|
|
305
|
+
**DO** test animations at 4x CPU throttle in Chrome DevTools. If frames drop below 60fps on throttled CPU, the animation is too expensive.
|
|
306
|
+
|
|
307
|
+
**DON'T** animate `box-shadow` directly — it is extremely expensive. Instead, animate `opacity` on a pseudo-element that has the shadow.
|
|
308
|
+
|
|
309
|
+
```css
|
|
310
|
+
/* Bad — repaints on every frame */
|
|
311
|
+
.card { transition: box-shadow 200ms ease-out; }
|
|
312
|
+
.card:hover { box-shadow: 0 20px 40px rgba(0,0,0,0.2); }
|
|
313
|
+
|
|
314
|
+
/* Good — opacity change is GPU composited */
|
|
315
|
+
.card { position: relative; }
|
|
316
|
+
.card::after {
|
|
317
|
+
content: '';
|
|
318
|
+
position: absolute;
|
|
319
|
+
inset: 0;
|
|
320
|
+
border-radius: inherit;
|
|
321
|
+
box-shadow: 0 20px 40px rgba(0,0,0,0.2);
|
|
322
|
+
opacity: 0;
|
|
323
|
+
transition: opacity 200ms ease-out;
|
|
324
|
+
}
|
|
325
|
+
.card:hover::after { opacity: 1; }
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
---
|
|
329
|
+
|
|
330
|
+
## Reduced Motion
|
|
331
|
+
|
|
332
|
+
The `prefers-reduced-motion` media query is not optional — it is a mandatory accessibility requirement. Users who set this preference include people with vestibular disorders (motion sickness from animation), epilepsy risk, and cognitive disabilities where motion creates distraction.
|
|
333
|
+
|
|
334
|
+
### The Two Categories
|
|
335
|
+
|
|
336
|
+
**Decorative motion** (remove entirely): Entrance animations, hover effects, parallax, background animations, staggered reveals. These serve aesthetic purposes only. Disable them completely.
|
|
337
|
+
|
|
338
|
+
**Functional motion** (keep, but simplify): Toast notifications sliding in, modal appearing to indicate focus shift, loading spinners. These communicate state — remove them and the user loses information. Replace movement with instant opacity transitions instead.
|
|
339
|
+
|
|
340
|
+
```css
|
|
341
|
+
@media (prefers-reduced-motion: reduce) {
|
|
342
|
+
/* Remove all decorative animations */
|
|
343
|
+
.card-enter,
|
|
344
|
+
.page-transition,
|
|
345
|
+
.stagger-item {
|
|
346
|
+
animation: none;
|
|
347
|
+
transition: none;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/* Simplify functional animations to opacity only */
|
|
351
|
+
.modal {
|
|
352
|
+
transition: opacity var(--duration-micro) linear;
|
|
353
|
+
/* No transform — just fade */
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
.toast {
|
|
357
|
+
transition: opacity var(--duration-micro) linear;
|
|
358
|
+
/* Remove translateY slide */
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
### Global Reset Pattern
|
|
364
|
+
|
|
365
|
+
```css
|
|
366
|
+
/* Apply to all elements when user prefers reduced motion */
|
|
367
|
+
@media (prefers-reduced-motion: reduce) {
|
|
368
|
+
*,
|
|
369
|
+
*::before,
|
|
370
|
+
*::after {
|
|
371
|
+
animation-duration: 0.01ms !important;
|
|
372
|
+
animation-iteration-count: 1 !important;
|
|
373
|
+
transition-duration: 0.01ms !important;
|
|
374
|
+
scroll-behavior: auto !important;
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
```
|
|
378
|
+
|
|
379
|
+
### React Hook
|
|
380
|
+
|
|
381
|
+
```tsx
|
|
382
|
+
function useReducedMotion(): boolean {
|
|
383
|
+
const [prefersReduced, setPrefersReduced] = React.useState<boolean>(() => {
|
|
384
|
+
if (typeof window === 'undefined') return false;
|
|
385
|
+
return window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
React.useEffect(() => {
|
|
389
|
+
const mediaQuery = window.matchMedia('(prefers-reduced-motion: reduce)');
|
|
390
|
+
const handler = (e: MediaQueryListEvent): void => setPrefersReduced(e.matches);
|
|
391
|
+
mediaQuery.addEventListener('change', handler);
|
|
392
|
+
return () => mediaQuery.removeEventListener('change', handler);
|
|
393
|
+
}, []);
|
|
394
|
+
|
|
395
|
+
return prefersReduced;
|
|
396
|
+
}
|
|
397
|
+
```
|
|
398
|
+
|
|
399
|
+
```tsx
|
|
400
|
+
function AnimatedCard({ children }: { children: React.ReactNode }) {
|
|
401
|
+
const reducedMotion = useReducedMotion();
|
|
402
|
+
|
|
403
|
+
return (
|
|
404
|
+
<motion.div
|
|
405
|
+
initial={reducedMotion ? false : { opacity: 0, y: 12 }}
|
|
406
|
+
animate={{ opacity: 1, y: 0 }}
|
|
407
|
+
transition={reducedMotion ? { duration: 0 } : { duration: 0.25 }}
|
|
408
|
+
>
|
|
409
|
+
{children}
|
|
410
|
+
</motion.div>
|
|
411
|
+
);
|
|
412
|
+
}
|
|
413
|
+
```
|
|
414
|
+
|
|
415
|
+
### DO / DON'T
|
|
416
|
+
|
|
417
|
+
**DO** test your UI with `prefers-reduced-motion: reduce` enabled in OS settings before every release.
|
|
418
|
+
|
|
419
|
+
**DON'T** use `prefers-reduced-motion` as an afterthought or accessibility checkbox. Build it into your animation system from the start so it applies automatically.
|
|
420
|
+
|
|
421
|
+
---
|
|
422
|
+
|
|
423
|
+
## Perceived Performance
|
|
424
|
+
|
|
425
|
+
Perceived performance is how fast your UI feels, independent of how fast it actually is. A 2-second load that shows content progressively feels faster than a 1-second load that shows nothing then dumps everything at once.
|
|
426
|
+
|
|
427
|
+
### Skeleton Screens
|
|
428
|
+
|
|
429
|
+
Replace empty loading states with skeletons that match the layout of the real content. The user understands what is loading and can orient their attention.
|
|
430
|
+
|
|
431
|
+
```tsx
|
|
432
|
+
function CardSkeleton() {
|
|
433
|
+
return (
|
|
434
|
+
<div className="p-6 rounded-xl border border-zinc-200 space-y-3 animate-pulse">
|
|
435
|
+
<div className="h-4 w-3/4 bg-zinc-200 rounded" />
|
|
436
|
+
<div className="h-3 w-full bg-zinc-100 rounded" />
|
|
437
|
+
<div className="h-3 w-2/3 bg-zinc-100 rounded" />
|
|
438
|
+
<div className="h-8 w-24 bg-zinc-200 rounded-lg mt-4" />
|
|
439
|
+
</div>
|
|
440
|
+
);
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
function CardList({ isLoading, cards }: { isLoading: boolean; cards: Card[] }) {
|
|
444
|
+
if (isLoading) {
|
|
445
|
+
return (
|
|
446
|
+
<div className="grid gap-6" style={{ gridTemplateColumns: 'repeat(auto-fit, minmax(280px, 1fr))' }}>
|
|
447
|
+
{Array.from({ length: 6 }).map((_, i) => <CardSkeleton key={i} />)}
|
|
448
|
+
</div>
|
|
449
|
+
);
|
|
450
|
+
}
|
|
451
|
+
return (
|
|
452
|
+
<div className="grid gap-6" style={{ gridTemplateColumns: 'repeat(auto-fit, minmax(280px, 1fr))' }}>
|
|
453
|
+
{cards.map(card => <Card key={card.id} {...card} />)}
|
|
454
|
+
</div>
|
|
455
|
+
);
|
|
456
|
+
}
|
|
457
|
+
```
|
|
458
|
+
|
|
459
|
+
### Optimistic UI
|
|
460
|
+
|
|
461
|
+
For actions with high confidence of success (creating a note, liking a post, toggling a setting), update the UI immediately and reconcile with the server response asynchronously. If the server returns an error, roll back with a toast notification.
|
|
462
|
+
|
|
463
|
+
```tsx
|
|
464
|
+
function useLike(postId: string) {
|
|
465
|
+
const [liked, setLiked] = React.useState(false);
|
|
466
|
+
|
|
467
|
+
const toggleLike = async (): Promise<void> => {
|
|
468
|
+
const previous = liked;
|
|
469
|
+
setLiked(!liked); // optimistic update — instant feedback
|
|
470
|
+
try {
|
|
471
|
+
await api.toggleLike(postId);
|
|
472
|
+
} catch {
|
|
473
|
+
setLiked(previous); // rollback on error
|
|
474
|
+
toast.error('Could not update like — try again');
|
|
475
|
+
}
|
|
476
|
+
};
|
|
477
|
+
|
|
478
|
+
return { liked, toggleLike };
|
|
479
|
+
}
|
|
480
|
+
```
|
|
481
|
+
|
|
482
|
+
---
|
|
483
|
+
|
|
484
|
+
## Scroll-Driven Animations
|
|
485
|
+
|
|
486
|
+
The CSS Scroll-Driven Animations API links animation progress directly to scroll position without JavaScript. It runs entirely on the compositor thread — no `scroll` event listeners, no `requestAnimationFrame`, no main thread cost.
|
|
487
|
+
|
|
488
|
+
### `animation-timeline: scroll()`
|
|
489
|
+
|
|
490
|
+
`scroll()` links animation progress to the scroll position of a scroll container.
|
|
491
|
+
|
|
492
|
+
```css
|
|
493
|
+
/* Progress bar that fills as the user scrolls the page */
|
|
494
|
+
.scroll-progress {
|
|
495
|
+
position: fixed;
|
|
496
|
+
top: 0;
|
|
497
|
+
left: 0;
|
|
498
|
+
height: 3px;
|
|
499
|
+
width: 100%;
|
|
500
|
+
background: linear-gradient(to right, #3b82f6, #8b5cf6);
|
|
501
|
+
transform-origin: left;
|
|
502
|
+
animation: progress-grow linear;
|
|
503
|
+
animation-timeline: scroll(root block);
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
@keyframes progress-grow {
|
|
507
|
+
from { transform: scaleX(0); }
|
|
508
|
+
to { transform: scaleX(1); }
|
|
509
|
+
}
|
|
510
|
+
```
|
|
511
|
+
|
|
512
|
+
### `animation-timeline: view()`
|
|
513
|
+
|
|
514
|
+
`view()` links animation progress to how much of an element is visible in the viewport. This is the correct replacement for IntersectionObserver reveal patterns.
|
|
515
|
+
|
|
516
|
+
```css
|
|
517
|
+
/* Element fades and translates in as it enters the viewport */
|
|
518
|
+
.reveal-on-scroll {
|
|
519
|
+
animation: reveal-item linear both;
|
|
520
|
+
animation-timeline: view();
|
|
521
|
+
animation-range: entry 0% entry 30%;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
@keyframes reveal-item {
|
|
525
|
+
from {
|
|
526
|
+
opacity: 0;
|
|
527
|
+
transform: translateY(24px);
|
|
528
|
+
}
|
|
529
|
+
to {
|
|
530
|
+
opacity: 1;
|
|
531
|
+
transform: translateY(0);
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
```
|
|
535
|
+
|
|
536
|
+
The `animation-range: entry 0% entry 30%` clause means the animation runs from when the element starts entering the viewport to when 30% of it is visible.
|
|
537
|
+
|
|
538
|
+
### React Integration
|
|
539
|
+
|
|
540
|
+
```tsx
|
|
541
|
+
// No JavaScript needed — pure CSS
|
|
542
|
+
// Apply the class and the browser handles the rest
|
|
543
|
+
|
|
544
|
+
function RevealSection({ children }: { children: React.ReactNode }) {
|
|
545
|
+
return (
|
|
546
|
+
<section className="reveal-on-scroll py-16">
|
|
547
|
+
{children}
|
|
548
|
+
</section>
|
|
549
|
+
);
|
|
550
|
+
}
|
|
551
|
+
```
|
|
552
|
+
|
|
553
|
+
### Browser Support and Fallback
|
|
554
|
+
|
|
555
|
+
Scroll-driven animations are supported in Chrome 115+ and Safari 18+. Firefox support is in progress. Always include a fallback:
|
|
556
|
+
|
|
557
|
+
```css
|
|
558
|
+
/* Base state — visible by default (fallback for unsupported browsers) */
|
|
559
|
+
.reveal-on-scroll {
|
|
560
|
+
opacity: 1;
|
|
561
|
+
transform: none;
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
/* Enhanced — scroll-driven animation for supported browsers */
|
|
565
|
+
@supports (animation-timeline: scroll()) {
|
|
566
|
+
.reveal-on-scroll {
|
|
567
|
+
opacity: 0;
|
|
568
|
+
transform: translateY(24px);
|
|
569
|
+
animation: reveal-item linear both;
|
|
570
|
+
animation-timeline: view();
|
|
571
|
+
animation-range: entry 0% entry 30%;
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
/* Always respect reduced motion */
|
|
576
|
+
@media (prefers-reduced-motion: reduce) {
|
|
577
|
+
.reveal-on-scroll {
|
|
578
|
+
animation: none !important;
|
|
579
|
+
opacity: 1 !important;
|
|
580
|
+
transform: none !important;
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
```
|
|
584
|
+
|
|
585
|
+
### DO / DON'T
|
|
586
|
+
|
|
587
|
+
**DO** use scroll-driven animations over JavaScript IntersectionObserver for simple reveal effects. The compositor-thread execution eliminates scroll jank.
|
|
588
|
+
|
|
589
|
+
**DON'T** use scroll-driven animations for complex stateful interactions (shopping cart updates, form validation, navigation). Those require JavaScript logic that cannot be expressed in CSS keyframes.
|
|
590
|
+
|
|
591
|
+
**DO** always pair `@supports (animation-timeline: scroll())` with a visible fallback state — unsupported browsers must not show hidden or broken content.
|