Skip to content

feat(roots): expressive motion tokens (spring easings + pop/shake/fade presets)#45

Merged
mattmaynes merged 3 commits into
mainfrom
expressive-motion-tokens
Jul 13, 2026
Merged

feat(roots): expressive motion tokens (spring easings + pop/shake/fade presets)#45
mattmaynes merged 3 commits into
mainfrom
expressive-motion-tokens

Conversation

@mattmaynes

Copy link
Copy Markdown
Contributor

Implements spec 0033 - curated expressive motion from transitions.dev, re-fitted to Canopy's minimal, opinionated scale (no wholesale import).

What's added (purely additive)

Type Tokens
Easings ease.spring cubic-bezier(0.34,1.36,0.64,1), ease.spring-strong cubic-bezier(0.34,3.85,0.64,1)
Durations duration.micro 80ms, duration.slower 480ms
Presets (animate-*) pop-in/pop-out (spring scale+fade), shake (error/invalid states), fade-in/fade-out (toasts/content)
  • Canopy had no overshoot easing - every existing curve decelerates. spring/spring-strong fill that gap.
  • Durations extend the scale at both ends; fast/base/slow are unchanged (transitions.dev's own fast=250/slow=400 would have collided, so we mapped intent, not literals).
  • Presets are hand-authored in preset-motion.css following the existing animate-dialog-*/-drawer-*/-bottom-sheet-* pattern exactly: @keyframes + a @theme block whose --animate-* values compose the --duration-*/--ease-* tokens (never hardcoded ms/easing), folded into dist/tailwind-preset.css. Consumers gate them with motion-reduce:animate-none.

Out of scope (deferred)

  • success-check and other multi-element composites (fade+rotate+blur+bob+SVG path-draw) - a single --animate-* is one keyframe timeline on one element; the check's path-draw needs stroke-dashoffset on a nested SVG path. That's component motion, not a token. Noted as future Branches work in the spec.
  • distance/scale/blur helper token families - separate decision.
  • No existing token or utility renamed/changed; no components retrofitted (each is its own follow-up).

Verification

  • pnpm build - new tokens land in dist/tokens.css (:root), dist/tokens.ts (typed), dist/tailwind-preset.css (@theme inline); the five animate-* utilities ship folded in.
  • pnpm test - 50/50 green; tokens.test.ts asserts the four tokens across all three outputs and that animate-pop-in composes var(--duration-base)+var(--ease-spring), animate-shake uses var(--ease-standard), animate-fade-in ships.
  • pnpm lint - clean (only the pre-existing eslint-plugin-react warning).

Docs: README motion section + docs/overview/features.md/architecture.md updated to match.

🤖 Generated with Claude Code

…e presets)

Adds curated motion from transitions.dev, re-fitted to Canopy's minimal scale:

- easings: ease.spring (0.34,1.36,0.64,1), ease.spring-strong (0.34,3.85,0.64,1)
  - Canopy's first overshoot curves; every existing easing decelerates.
- durations: micro (80ms) and slower (480ms) extend fast/base/slow without collision.
- presets (preset-motion.css keyframes + token-composed animate-*): pop-in/out (spring),
  shake (error/invalid states), fade-in/out (toasts/content). Each composes --duration-*/
  --ease-* and is gated by consumers with motion-reduce:animate-none.

Existing tokens and animate-dialog/drawer/bottom-sheet utilities are untouched (purely
additive). success-check and distance/scale/blur helpers deferred (see spec 0033).

Spec: docs/specs/0033-expressive-motion-tokens.md
Tests: tokens.test.ts asserts the new tokens across all three outputs and that the
animate-* utilities ship token-composed. pnpm build/test/lint green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YHabPujNkgKUUNJjaBQCgT
Comment thread packages/roots/tokens/motion.json
Comment thread packages/roots/tokens.test.ts Outdated
Comment thread packages/roots/tokens/motion.json
Comment thread packages/roots/preset-motion.css
Comment thread packages/roots/tokens.test.ts Outdated
Comment thread packages/roots/tokens.test.ts Outdated
Comment thread packages/roots/preset-motion.css
Comment thread docs/overview/architecture.md
Comment thread packages/roots/tokens.test.ts Outdated
Comment thread packages/roots/tokens.test.ts Outdated
mattmaynes and others added 2 commits July 13, 2026 08:30
Persona review of PR #45 (designer/engineer/tester/architect):

- tests (tester+engineer, major): assert BOTH halves of every preset pair (pop-out,
  fade-out were unverified) and that each --animate-* composes its tokens, via a
  per-variant loop; add an occurrence-count guard proving the fold stays idempotent
  (spec 0033 acceptance item that had no test). fade-in now asserts composition, not
  just presence.
- shake a11y (designer, major): reduced-motion gating is now documented as MANDATORY
  (not optional) for animate-shake in the preset header + README - it is a vestibular
  trigger.
- doc drift (architect, minor): reframe the preset-motion.css header and architecture.md
  from "overlay-motion" to keyframed motion (overlay + expressive), now that generic
  pop/shake/fade presets live there too.

Learning captured: docs/feedback/0015-assert-every-shipped-variant-and-acceptance-property.md
+ overview/learnings.md (second coverage-gap review after 0014).

Open for developer decision: spring-strong (designer major) - kept as opted-in; see PR reply.

pnpm build/test/lint green (50/50).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YHabPujNkgKUUNJjaBQCgT
Developer decision on the designer's major (PR #45): keep ease.spring-strong but dial
the overshoot from 3.85 to cubic-bezier(0.34, 2.2, 0.64, 1) - still a pronounced bounce,
but on-brand for a calm system. Token, tests, and spec 0033 updated in lockstep.

pnpm build/test/lint green (50/50).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YHabPujNkgKUUNJjaBQCgT
@mattmaynes mattmaynes merged commit 28b2d0a into main Jul 13, 2026
1 check passed
@mattmaynes mattmaynes deleted the expressive-motion-tokens branch July 13, 2026 22:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant