Add motion design tokens (duration and easing) to @wordpress/theme#76097
Conversation
|
Size Change: +331 B (0%) Total Size: 7.93 MB 📦 View Changed
ℹ️ View Unchanged
|
mirka
left a comment
There was a problem hiding this comment.
Here's some context on why I'm hesitant to add motion tokens in this manner.
- If motion is not going to be themed dynamically through
ThemeProvider, it isn't necessary to manage as a official design token. (Which is good for avoiding token bloat in the stylesheet. But @aduth has suggested that we can manage tokens in the theme package as a central "data store", while still omitting them from the tokens stylesheet, so that could be a mitigation option.) - Most motion is already encapsulated at relatively higher levels (e.g. Tooltip/Dialog components, dropdown motion module, focus module). Which is both sufficient and easier for motion sharing across similar components.
- At the moment, there are Framer Motion usages that can't use values from CSS variables (not a blocker per se, but needs some planning on what to do).
So motion is one of those categories where I think we need to weight the cost–benefit. Maybe my intuition of the cost is overblown, and benefit underestimated.
These are just my hesitations — open to everyone's thoughts!
|
The following accounts have interacted with this PR and/or linked issues. I will continue to update these lists as activity occurs. You can also manually ask me to refresh this list by adding the Unlinked AccountsThe following contributors have not linked their GitHub and WordPress.org accounts: @nick-a8c. Contributors, please read how to link your accounts to ensure your work is properly credited in WordPress releases. If you're merging code through a pull request on GitHub, copy and paste the following into the bottom of the merge commit message. To understand the WordPress project's expectations around crediting contributors, please review the Contributor Attribution page in the Core Handbook. |
|
I defer to y'all on the implementation :) What's not clear to me is how we'd ensure consistent profiles (easing, duration) across components without tokens. Is that something we'd have to maintain manually?
Could there be a world where it is theme-able? For instance would it make sense for |
I think this strikes at an interesting question about what values make sense to be a design token. This interpretation reads to me as being based on the ability to customize, though I've personally approached it more as encoding design decisions that apply uniformly across components, that we can update in one place and cascade (single source of truth), and that can be used in the design process (e.g. Figma variables) for better code/design parity. In particular, the way these are being proposed feel to me like they aren't tied to any one specific component but apply generally across the system. I'm not as sure about design tooling, but it feels like a value we'd want to expose and use in motion design. Or maybe reframing: What is it about what's said about how we can manage motion through higher-level abstractions that is untrue about how we think about typography or color, for example? |
I guess a large part of my skepticism is about the validity of encoding the motion design decisions at the individual CSS property level.
|
|
Another fun thought that came to mind in support of higher-level encapsulation! Let's say we had an "intensity" setting for motion, where high intensity is very fun and low intensity is more calm. Could this be achieved globally by changing token values at a CSS property level? I'd say no. Each motion module (tooltips, dialogs, etc) needs to tweak itself appropriately for each intensity level, which would probably be switched by entire declaration blocks via a data attribute. |
Doesn't it make sense to handle this via ThemeProvider though, like all other theming aspects? With higher-level encapsulation could motion still be themeable if we introduced and utilised a wider range of motion primitives in the theme package, maybe with modes for different styles? |
|
I see both sides of the argument. Personally, I like the idea that shared variables all come as DS tokens. If we keep the stylesheet around, we don't really have to include these tokens in |
It will be handled by rather than: So while tokens could be beneficial if it's important to "quantize" durations and easings across the ecosystem to a certain set of values, it's not a requirement for overall motion consistency or motion theming. Consistency and theming will need to be addressed at higher levels anyway. |
|
Would it not be useful to have a set of tokens to utilise like so? This would give third parties building custom components a set of tokens to maintain coherent animation profiles with the system (and inherit any updates we make automatically). Or is the point that tokens like Sorry if I'm missing something obvious. |
Yes, if the point is to quantize values across the system, I'm not against that or anything. Let's try it out. |
|
I think the duration tokens are probably okay for a start, but how do you feel about the easing tokens? I'm wondering if there should be more of a scale, e.g. |
I have no idea actually. I think it's something that needs to be factored out of the actual animations that are going to be used? In other words, if we aren't sure yet, it could be fine to leave it out for now and add the tokens when we have a better idea of what is needed. |
|
Heh, I (softly) take the opposite position where the system provides some guardrails that inform what's possible rather than trying to extract tokens from patterns. I worry the latter could result in a lack of cohesion. Because the tokens would be semantic, introducing something lightweight now shouldn’t lock us in. We could start consuming them in a few prominent examples (Modal/Dialog, Menu, etc.) – maybe even in this PR to test – then iterate (and expand the tokens) later as needed. |
|
Sure, I'm fine with that too 👍 |
|
I updated |
d234848 to
93122b3
Compare
|
Flaky tests detected in 450e37f. 🔍 Workflow run URL: https://github.com/WordPress/gutenberg/actions/runs/25430476251
|
There was a problem hiding this comment.
Probably makes sense to move this file to the root storybook folder so we can just use @wordpress/ui components and avoid a lot of custom CSS?
There was a problem hiding this comment.
Do you mean just this file or the whole story?
There was a problem hiding this comment.
I think I worked out what you meant, and updated accordingly. We'll need to consolidate our separate token folders at some point, but that doesn't need to happen here(?).
If I got it wrong let me know 😅
There was a problem hiding this comment.
Yeah my wording was off. What you did is correct, thanks 😄
| const EASING_TOKENS = [ | ||
| { | ||
| name: 'standard', | ||
| variable: 'var(--wpds-motion-easing-standard)', | ||
| description: 'State changes like hover, color, and toggle transitions.', | ||
| }, | ||
| { | ||
| name: 'decelerate', | ||
| variable: 'var(--wpds-motion-easing-decelerate)', | ||
| description: | ||
| 'Elements entering the screen, such as menus and popovers.', | ||
| }, |
There was a problem hiding this comment.
It isn't great that we need to define these manually when the data is all already available in the token store, but just not fully exposed in @wordpress/theme/design-tokens.js. Not a blocker, but I will see what we can do to make this easier.
There was a problem hiding this comment.
Coincidentally, this ☝️ can also help us with the token usage in all the JS files! We can expose the actual token data, not just the metadata. Win.
There was a problem hiding this comment.
Yeah my wording was off. What you did is correct, thanks 😄
| const EASING_TOKENS = [ | ||
| { | ||
| name: 'standard', | ||
| variable: 'var(--wpds-motion-easing-standard)', | ||
| description: 'State changes like hover, color, and toggle transitions.', | ||
| }, | ||
| { | ||
| name: 'decelerate', | ||
| variable: 'var(--wpds-motion-easing-decelerate)', | ||
| description: | ||
| 'Elements entering the screen, such as menus and popovers.', | ||
| }, |
There was a problem hiding this comment.
Coincidentally, this ☝️ can also help us with the token usage in all the JS files! We can expose the actual token data, not just the metadata. Win.
2f2b871 to
ffcefc6
Compare
Made-with: Cursor
Rename the easing tokens to describe their visual weight rather than their technical curve behavior: - gentle → subtle - standard → balanced - decelerate → expressive - decelerate-emphasized → dramatic Made-with: Cursor
Remove the non-existent `css-modules` type package from tsconfig and add a local type declaration file for `.module.css` imports instead. Made-with: Cursor
Merge `dramatic` into `expressive` and adjust all three curves for better perceptual separation: `subtle` is now near-linear, `balanced` remains the general-purpose curve, and `expressive` covers all enter/exit and spatial transitions. Made-with: Cursor
The Storybook Vite setup doesn't serve CSS modules correctly from the storybook/stories/ directory. Use inline styles and an injected style tag for keyframes instead, consistent with other design system stories. Made-with: Cursor
Add entries for components and ui packages noting the adoption of --wpds-motion-* design tokens in Modal, Menu, DropdownMenu, and Dialog. Made-with: Cursor
React supports rendering string children inside <style> tags directly, so we can drop dangerouslySetInnerHTML (and its eslint-disable comment). Co-authored-by: Cursor <cursoragent@cursor.com>
Terrazzo's CSS duration transform expects $value as the object form
{ value, unit } per the DTCG spec. The string form ("400ms") caused
the generator to emit 'undefinedundefined' for both the CSS variables
and the JS fallback map, which left consumers (such as the motion
Storybook story) with effectively zero-duration animations.
Switching motion.json to the object form and regenerating the prebuilt
files restores correct values throughout.
Co-authored-by: Cursor <cursoragent@cursor.com>
After rebasing onto trunk, relocate PR #76097 notes from shipped version sections into each package Unreleased section (consolidate duplicate components entries). Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Explain that FRAME_ANIMATION_DURATION_MS (from CONFIG.transitionDuration) must stay aligned with --wpds-motion-duration-md on the modal frame in SCSS. Co-authored-by: Cursor <cursoragent@cursor.com>
Use --wpds-motion-duration-xs as the scrim fade-out delay so xs+sm finishes before the frame’s md disappear animation (unmount timing). Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
.popup already defines the same opacity/transform transitions; the nested [data-open] rule was redundant. Co-authored-by: Cursor <cursoragent@cursor.com>
- Replace components SelectControl/TextControl/Button with Field, Select, InputControl, and brand-tone Button from @wordpress/ui. - Drop design-system-tokens package-styles entry; stories still match the broader design-system rule (design tokens only, no duplicate bundle). Co-authored-by: Cursor <cursoragent@cursor.com>
450e37f to
66945cb
Compare
ciampo
left a comment
There was a problem hiding this comment.
LGTM 🚀
A couple of caveats / potential final changes / follow-ups:
easing-balancedis not used yet by any components — I assume we'll be able to make changes if needed, but we may also consider not exporting the token at all until we have a need for it?- I'll put emphasis once again on the duplication of values for modal and dropdown animations (
config-values.js,use-modal-exit-animation.ts,dropdown-motion.ts,@wordpress/themetokens). The obivous long-term fix is to either parse the DS token value in JS, or switch to CSS/Sass-based styles (so that the token can be consumed directly). - we should consider including support for
prefers-reduced-motiondirectly at the token level — although that should be discussed/specced/implemented in a follow-up
|
Of these points the first seems most suitable to consider addressing in this PR. I don't think it would hurt to export – I suspect it will get used relatively soon, and it rounds out the set for third party consumers. I think there are several unused tokens exported already so it's not a new precedent. |
Let's merge and iterate 👌 |
Three Unreleased entries were inadvertently removed when the Draggable migration commit was rebased onto trunk: - @wordpress/components Internal: `Modal`, `Menu`, `DropdownMenu` motion-token adoption (#76097). - @wordpress/components Internal: `Popover` close-button z-index cleanup (#78180). - @wordpress/ui Bug Fixes: `Text` CSS-defense values for paragraph and heading variants (#78172). Restore them under their original headings.
Three Unreleased entries were inadvertently removed when the Draggable migration commit was rebased onto trunk: - @wordpress/components Internal: `Modal`, `Menu`, `DropdownMenu` motion-token adoption (#76097). - @wordpress/components Internal: `Popover` close-button z-index cleanup (#78180). - @wordpress/ui Bug Fixes: `Text` CSS-defense values for paragraph and heading variants (#78172). Restore them under their original headings.
Three Unreleased entries were inadvertently removed when the Draggable migration commit was rebased onto trunk: - @wordpress/components Internal: `Modal`, `Menu`, `DropdownMenu` motion-token adoption (#76097). - @wordpress/components Internal: `Popover` close-button z-index cleanup (#78180). - @wordpress/ui Bug Fixes: `Text` CSS-defense values for paragraph and heading variants (#78172). Restore them under their original headings.
Three Unreleased entries were inadvertently removed when the Draggable migration commit was rebased onto trunk: - @wordpress/components Internal: `Modal`, `Menu`, `DropdownMenu` motion-token adoption (#76097). - @wordpress/components Internal: `Popover` close-button z-index cleanup (#78180). - @wordpress/ui Bug Fixes: `Text` CSS-defense values for paragraph and heading variants (#78172). Restore them under their original headings.
…ot (#78183) * Draggable: Migrate clone wrapper to wp compat overlay slot Replace the legacy body-level / element-wrapper placement and its `z-index: 1000000000` with a portal-style migration onto the `@wordpress/ui` compat overlay slot (#77851). When the slot is available, the drag clone joins the slot's body-level stacking context across all three placement modes, so an active drag automatically shares stacking with any `@wordpress/ui` overlay opened mid-drag without needing per-version z-index races. Auto-enabled in WordPress environments via the slot helper's `window.wp.components` auto-detect; standalone hosts that bundle `@wordpress/components` directly fall back to the previous placement until they call `useEnableWpCompatOverlaySlot()`. `@wordpress/components` imports `getWpCompatOverlaySlot()` directly from `@wordpress/ui`'s public exports (also promoted from internal in this change). The `@wordpress/components` dep on `@wordpress/ui` is transitional, scoped to the legacy-overlay migration. Cross-document drags (e.g. dragging an element inside an iframe while the slot is in the parent document) fall back to the previous placement so the clone's viewport-relative geometry stays in a single coordinate space. The default placement mode (`appendToOwnerDocument: false`, no `dragComponent`) previously appended the clone to the dragged element's parent. In WP environments where the slot is now in effect, the clone instead lives in the slot — a body-level location. In-repo ripgrep finds no CSS or event-delegation scoping anchored to the clone's previous in-flow parent; external consumers that relied on that ancestry must either not opt into the slot or migrate their scoping. * Draggable: Storybook: render docs-page stories in iframes The drag clone uses `position: fixed`, which Storybook's docs-page wrappers break because they apply `transform`s that establish new containing blocks. As a result the clone resolves against those wrappers instead of the viewport and lands in the wrong place on the autodocs page. Render each Draggable story in its own iframe on the autodocs page via `parameters.docs.story.inline: false`, which restores viewport-relative positioning for the clone. * Draggable: Storybook: polish cross-document fallback playground story Three small follow-ups on the iframe regression story: - Inject the Draggable SCSS into the iframe via Vite's `?inline` import (same pattern `WithGlobalCSS` uses with `global-basic.scss?inline`) instead of duplicating rule bodies in `srcDoc`. Single source of truth; future SCSS edits flow through automatically. - Guard the style injection on `iframeDoc?.head` so the brief about:blank → srcDoc transition doesn't throw on the initial `useEffect` pass. - Align the slot-presence display with the public `getWpCompatOverlaySlot()` API: it now returns `undefined` rather than `null` when no slot is registered. * CHANGELOG: Restore entries dropped during rebase Three Unreleased entries were inadvertently removed when the Draggable migration commit was rebased onto trunk: - @wordpress/components Internal: `Modal`, `Menu`, `DropdownMenu` motion-token adoption (#76097). - @wordpress/components Internal: `Popover` close-button z-index cleanup (#78180). - @wordpress/ui Bug Fixes: `Text` CSS-defense values for paragraph and heading variants (#78172). Restore them under their original headings. * Storybook: Fix popover-with-slotfill cross-iframe collision boundary `Popover.Popup` stopped accepting `collisionBoundary` directly when #78168 introduced the `Popover.Positioner` slot subcomponent. The prop is now silently ignored on `Popup`, so the cross-iframe story's collision avoidance regressed after the rebase onto trunk. Route the boundary through `Popover.Positioner` (matching the modern `Popover.Popup`'s `positioner` slot pattern) so the popup honors the iframe's clipping edge again. This file is `.jsx` so the type system didn't catch the silent prop-drop. * Draggable: Storybook: refresh AppendElementToOwnerDocument JSDoc The story's JSDoc still described the legacy "escape ancestor stacking context" rationale, which now contradicts the updated `appendToOwnerDocument` JSDoc in `types.ts` for hosts that opt into the `@wordpress/ui` compat overlay slot — where the clone always lives in the body-level slot and the prop is a no-op. Update the story's docblock to mirror the type-level guidance and call out the cross-document fallback exception. * Draggable: CHANGELOG: Call out default-mode in-flow ancestor change The original Draggable entry covered the stacking + cross-document fallback story, but left the load-bearing behavior change for third-party consumers in the PR description only: in the default placement mode (no `appendToOwnerDocument`, no `__experimentalDragComponent`), the clone used to be a DOM descendant of the dragged element's parent. With the slot active, it now lives at the body-level slot regardless. Surface that change directly in the CHANGELOG entry, including a migration hint for consumers that scoped CSS or event delegation on the clone's previous ancestry. * Draggable: Add e2e regression for chip-inside-compat-slot Lock in the structural guarantee that underpins the stacking claim in #78183: when `@wordpress/components`'s `Draggable` runs in a WordPress environment, the drag chip is rendered inside the body-level `[data-wp-compat-overlay-slot]`. That single structural assertion subsumes the visual stacking contract — the slot creates an isolated stacking context with `z-index: 1000000003`, so anything appended into it stacks above any `@wordpress/components` overlay opened mid-drag (which live outside the slot at lower `z-index`s). Asserting structure rather than visual layering keeps the test robust against unrelated overlay z-index churn, and avoids a brittle `elementFromPoint`-style probe across the parent-doc/canvas-iframe boundary. * Storybook: Trim file-level docblocks on playground stories Drop the file-level brain-dump JSDoc from the `draggable-cross-document-fallback` and `popover-with-slotfill` playground stories. The story body and any per-story copy carry the user-facing explanation; the file-level prose was internal reasoning that doesn't belong in the story source. Per mirka's review on PR #78183 (empty-suggestion blocks). * Storybook: popover-with-slotfill story: use public @wordpress/ui API Switch the playground story to consume `Popover` from the public `@wordpress/ui` entry point instead of reaching into `packages/ui/src/popover`. Inline a small `IframePortal` helper locally so the story no longer depends on `packages/ui/src/popover/stories/utils` either (those story utilities are not part of any public surface). Also swap the `Slot` ref from `useRef` to a state setter so the popup re-renders once the slot's container element mounts, which removes a first-render race the previous `useRef` pattern had. Per mirka's review on PR #78183. * Storybook: draggable cross-doc story: load components styles via Storybook bundle Swap the iframe's style injection from a `?inline` import of `packages/components/src/draggable/style.scss` (reaching into another package's source) to Storybook's own `storybook/package-styles/components-ltr.lazy.scss`, which is the canonical bundle of `@wordpress/components` styles for stories. The injected CSS is now broader than strictly necessary (the whole package stylesheet rather than only Draggable's rules), but this is a debug fixture and the cost is negligible. In exchange we drop the cross-package src reach. Per mirka's review on PR #78183. * Storybook: Move cross-document fallback story under "Debug fixtures" The cross-document fallback story is strictly defensive regression coverage and doesn't illustrate a pattern non- maintainers would seek out. Move it under a `Debug fixtures` sub-section in the sidebar so the main `Playground/` namespace stays focused on intended-usage demos. Per mirka's review on PR #78183. * Storybook: Drop redundant `parameters.sourceLink` from playground stories The `source-link` Storybook addon already derives the GitHub source path from `storyData.importPath` when no explicit `parameters.sourceLink` is provided (see `storybook/addons/source-link/manager.ts`). For stories living under `storybook/stories/playground/`, that fallback resolves to the same value the explicit `sourceLink` was hard-coding, so the declaration is pure duplication. Per mirka's review on PR #78183 (empty-suggestion blocks covering the `parameters: { sourceLink: ... }` literal). * Draggable: Migrate styles from SCSS to a CSS module Move the (already small) Draggable stylesheet to a CSS module so its rules travel via `@wordpress/style-runtime` (and therefore into any iframe wrapped in `<StyleProvider>` — e.g. the block-editor canvas) without needing the package-level `build-style/style.css` bundle. Drops the `@use` line from `packages/components/src/style.scss`, following the same shape as the `AlignmentMatrixControl` (#73714/#73757) and `AnglePickerControl` (#73786) migrations. The CSS-module class names are standard (hashed). The legacy `components-draggable__*` / `is-dragging-components-draggable` class names are kept by adding them alongside the hashed ones in the JS `classList.add(...)` calls, since several other Gutenberg packages reference them in their own stylesheets (block-editor's `list-view`, `block-tools`, `block-library`'s `navigation` editor, `edit-widgets`' `widget-area` editor) and block-editor runtime JS reads `is-dragging-components-draggable` off `document.body`. Dropping those names would silently break those consumers. Per mirka's review on PR #78183 (CSS-module option for the iframe story); the corresponding Storybook simplification follows in a separate commit. * Storybook: Simplify cross-document fallback story with StyleProvider Now that Draggable's styles ship as a CSS module routed through `@wordpress/style-runtime`, the cross-document fallback story no longer needs to manually `?inline`-import and inject the whole `components-ltr` SCSS bundle into the iframe's `<head>`. Wrap the portaled iframe content in `<StyleProvider document={iframeDoc}>` from `@wordpress/components` instead — `StyleProvider` calls `registerDocument()` on the iframe document, and the style registry replays every registered CSS module (Draggable included) into that document. The visible behavior is unchanged: the orange clone still tracks the cursor inside the iframe, demonstrating the cross-document fallback. Per mirka's review on PR #78183. * Draggable: CHANGELOG: Move entry to Unreleased and slim it down The Enhancements entry for this PR ended up rolled into the already-cut `33.1.0` release section during an earlier rebase, and had grown to a 700-character paragraph spelling out every edge case (cross-document fallback, `appendToOwnerDocument` semantics, in-flow ancestor migration hints). Move it back to `## Unreleased` and trim to a two-sentence summary in line with the surrounding entries. The dropped detail still lives in the JSDoc, the code comments, and the PR description's <details> blocks. * Draggable: Trim verbose inline code comments Sweep across the comments added by this PR, dropping redundant duplication, narration of self-evident code, and prose that already lives in the PR description / JSDoc: - Drop the duplicate compat-slot note from the `AppendElementToOwnerDocument` story JSDoc (the interaction is already described on the prop's TS JSDoc in `types.ts`). - Tighten the prop JSDoc for `appendToOwnerDocument` to a single short paragraph. - Slim the same-document-only slot guard comment in `Draggable.start()` (the conditional itself reads as "slot if same document"). - Compact the rationale comment for `parameters.docs.story.inline: false` in the Draggable autodocs config to a single explanation. - Trim the structural-stacking assertion comment in the Playwright `draggable-blocks` spec. - Drop the forward-looking "can be removed on a future Stylelint upgrade" note from `CSS_BASELINE_2024_FUNCTIONS`. No behavior change. * @wordpress/ui CHANGELOG: Move #78183 entry to Unreleased The `getWpCompatOverlaySlot()` export bullet was left inside the already-released `## 0.13.0` section when the parallel `@wordpress/components` entry was moved to `## Unreleased`. * Draggable: Keep physical `left` for the invisible drag image Mirroring this offscreen stand-in in RTL has no benefit — either side hides it equally — so revert to the original physical property and silence the logical-properties lint with a targeted comment. * @wordpress/ui CHANGELOG: Trim #78183 entry * @wordpress/ui: Restore unrelated tsconfig change * Storybook: Drop redundant story-name overrides * Draggable: Keep body cursor class global The cursor flip is also triggered by external code (block-editor keyboard drag, etc.) that toggles `is-dragging-components-draggable` directly. Targeting the legacy class globally preserves that flow, which a module-hashed selector silently broke. * Draggable: Guard class arrays against the Jest CSS-module mock `jest-preset-default`'s style mock returns `undefined` for any class, which `classList.add()` would coerce to a literal "undefined" token. Filter falsy entries to keep test DOM clean. * Draggable: Address minor self-review nits - Note why the invisible drag image bypasses the compat slot. - Drop a redundant chip-count assertion in the e2e spec. - Flag the SCSS-only stylelint override pattern explicitly. * Storybook: Group compat-slot fixtures under Debug fixtures Consolidates the manual verification stories (`WP Compat Overlay Slot`, `Popover with SlotFill`) alongside the existing Draggable fixture. * Draggable: Use kebab-case for CSS module class names --- Co-authored-by: ciampo <mciampini@git.wordpress.org> Co-authored-by: mirka <0mirka00@git.wordpress.org>
Screen.Recording.2026-03-03.at.16.33.21.mov
What
Adds motion design tokens to
@wordpress/theme— a set of duration and easing curve tokens for standardizing animation timing across components — and adopts them in Dialog, Modal, and Menu/DropdownMenu.Duration tokens
--wpds-motion-duration-xs50ms--wpds-motion-duration-sm100ms--wpds-motion-duration-md200ms--wpds-motion-duration-lg300ms--wpds-motion-duration-xl400msEasing tokens
--wpds-motion-easing-subtlecubic-bezier(0.15, 0, 0.15, 1)--wpds-motion-easing-balancedcubic-bezier(0.4, 0, 0.2, 1)--wpds-motion-easing-expressivecubic-bezier(0.25, 0, 0, 1)Why
Animation timing is currently hardcoded across components with magic numbers. The
Dialogcomponent, for example, uses0.2s cubic-bezier(0.29, 0, 0, 1)for its entrance and0.2s cubic-bezier(1, 0, 0.2, 1)for its exit — values that were chosen thoughtfully but aren't reusable by other components.Centralizing these values as tokens:
Design decisions
Easing model — decelerate for both enter and exit: Rather than providing separate accelerate/decelerate pairs, the system uses decelerating easing for both entrances and exits. This follows the logic that all enter/exit animations should start fast and feel responsive. Accelerating easing (slow start, fast end) can feel jarring for exits like fades and scale-downs, where the element lingers visibly before vanishing abruptly. Decelerating easing front-loads the visual change and finishes gently, which feels smoother for the opacity and scale-based animations used by dialogs, modals, and menus.
Three easing tokens, three roles: The easing tokens map to a simple decision tree:
expressivebalancedsubtlelinearkeyword (no token needed)Intent-based naming: Easing tokens are named by perceptual intensity (
subtle→balanced→expressive) rather than by curve direction or physics model. This keeps the API intuitive — you pick the token that matches how noticeable you want the easing to feel — and avoids overloading the scale with direction-modifier combinations.Perceptually distinct curves: The three curves are designed for clear separation across the scale.
subtleis near-linear, keeping transitions almost imperceptible.balancedhas a noticeable slow start with moderate deceleration.expressivehas a snappy deceleration that makes enter/exit transitions feel responsive. Each curve differs meaningfully from its neighbors in both halves of the animation.Duration scale: The five-step
xs–xlscale provides enough granularity without being overwhelming. The descriptions intentionally avoid prescribing specific components to specific durations, since the right choice depends on the element's size, prominence, and context.How
Token definition
packages/theme/tokens/motion.json(DTCG format, withdurationandcubicBeziertypes)terrazzo.config.tsto include the new file and generateDurationSizeandEasingTypeScript typesmotion.story.tsx) with interactive animation demosToken adoption
UI/Dialog (
packages/ui/src/dialog/style.module.css): Replaced all hardcoded duration+easing pairs. The backdrop uses--wpds-motion-easing-subtle(background opacity transition), while the popup uses--wpds-motion-easing-expressivefor both enter and exit.Components/Modal (
packages/components/src/modal/):--wpds-motion-easing-subtle(background opacity transition).--wpds-motion-easing-expressive.--modal-frame-animation-durationcustom property with--wpds-motion-duration-mdapplied directly in CSS, removingframeStylefrom the exit animation hook and the component.@wordpress/base-stylesfade mixins with local keyframes using--wpds-motion-duration-smfor the backdrop fade.Components/Menu and DropdownMenu (
packages/components/src/utils/dropdown-motion.ts,packages/ui/src/utils/css/dropdown-motion.module.css):dropdown-motion.module.css) now uses--wpds-motion-duration-md,--wpds-motion-easing-expressive, and--wpds-motion-duration-smtokens directly.dropdown-motion.ts) retain hardcoded values because theno-ds-tokensESLint rule prevents--wpds-*usage inpackages/components/src/(incompatible with Emotion's build-time fallback injection). A comment documents the constraint and notes that values should stay in sync with the tokens.Testing
npm run --workspace @wordpress/theme buildto verify the token build succeedsdesign-tokens.css