feat: replace animation-delay tokens with .sf-stagger class and hover knobs - #612
Conversation
…udes Replace the fixed --sf-animation-delay-1..5 stagger tokens with a single .sf-stagger class + one --sf-stagger-step knob. Put .sf-stagger on a parent and every direct child gets an incrementing animation-delay, so time-based entrance animations play in sequence with no manual per-item index and no cap on child count: where sibling-index() is supported the ramp is unbounded, otherwise an 8-step :nth-child fallback (covering a 4-column grid's first two rows) plateaus so long lists still animate. Each child's delay is index * --sf-stagger-step * --sf-motion-scale. Choreography only — a child without its own animation just carries an inert delay, so animating only some children needs no opt-out on the rest. Pairs with time-based animations; .sf-entrance/.sf-exit are scroll-driven (rhythm via animation-range). Also tokenize the .sf-hover-* transform utilities: --sf-hover-grow-scale, --sf-hover-shrink-scale, --sf-hover-lift, --sf-hover-slide — magnitudes were hard-coded literals, now one global knob per axis. Docs (llm-guide, motion, macros, demo, annotations), token snapshot, static coverage artifacts, generated indexes and the demo override generator updated; new motion.spec coverage. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TsAowhRHNTVVfxvafVtDas
📝 WalkthroughWalkthroughAdds ChangesMotion utilities and public token contract
Estimated code review effort: 3 (Moderate) | ~25 minutes Possibly related issues
Possibly related PRs
Suggested labels: Suggested reviewers: 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Greptile SummaryReplaces five
Confidence Score: 4/5Safe to merge; the only findings are a missing The CSS logic, core/motion.css — the Important Files Changed
Flowchart%%{init: {'theme': 'neutral'}}%%
flowchart TD
A[".sf-stagger parent"] --> B{"@supports\nsibling-index()?"}
B -- Yes --> C["--sf-stagger-i =\nsibling-index() - 1\n(unbounded)"]
B -- No --> D["Fallback: 8-step\n:nth-child() ramp"]
D --> E["child 1-8:\n--sf-stagger-i = 0…7"]
D --> F["child 9+:\n--sf-stagger-i = 8\n(plateau)"]
C --> G["animation-delay =\n--sf-stagger-i × --sf-stagger-step × --sf-motion-scale"]
E --> G
F --> G
G --> H{"prefers-reduced-motion:\nno-preference?"}
H -- Yes --> I["Delay applied ✓"]
H -- No --> J["Entire .sf-stagger block\nnot rendered (gated)"]
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
flowchart TD
A[".sf-stagger parent"] --> B{"@supports\nsibling-index()?"}
B -- Yes --> C["--sf-stagger-i =\nsibling-index() - 1\n(unbounded)"]
B -- No --> D["Fallback: 8-step\n:nth-child() ramp"]
D --> E["child 1-8:\n--sf-stagger-i = 0…7"]
D --> F["child 9+:\n--sf-stagger-i = 8\n(plateau)"]
C --> G["animation-delay =\n--sf-stagger-i × --sf-stagger-step × --sf-motion-scale"]
E --> G
F --> G
G --> H{"prefers-reduced-motion:\nno-preference?"}
H -- Yes --> I["Delay applied ✓"]
H -- No --> J["Entire .sf-stagger block\nnot rendered (gated)"]
Reviews (1): Last reviewed commit: "feat(motion): add .sf-stagger utility an..." | Re-trigger Greptile |
| .sf-stagger > * { | ||
| animation-delay: calc( | ||
| var(--sf-stagger-i, 0) * var(--sf-stagger-step) * var(--sf-motion-scale) | ||
| ); | ||
| } |
There was a problem hiding this comment.
The
var(--sf-stagger-step) and var(--sf-motion-scale) references carry no fallback values, unlike every hover-knob reference in optional/utilities.css which all use var(--sf-hover-*, default). If motion.css is ever loaded standalone (or a token is accidentally unset), the whole calc() expression becomes invalid and animation-delay silently resolves to 0s — no stagger, no error. Adding the same defaults that tokens.css declares keeps the rule self-contained and consistent with the pattern established in utilities.css.
| .sf-stagger > * { | |
| animation-delay: calc( | |
| var(--sf-stagger-i, 0) * var(--sf-stagger-step) * var(--sf-motion-scale) | |
| ); | |
| } | |
| .sf-stagger > * { | |
| animation-delay: calc( | |
| var(--sf-stagger-i, 0) * var(--sf-stagger-step, 75ms) * var(--sf-motion-scale, 1) | |
| ); | |
| } |
| - **tokens:** the `.sf-hover-*` transform utilities now read magnitude knobs instead of hard-coded values — `--sf-hover-grow-scale` (1.05), `--sf-hover-shrink-scale` (0.95), `--sf-hover-lift` (0.25em, float/sink), `--sf-hover-slide` (0.5em, slide-start/end) — so each effect's strength is one global override. | ||
|
|
||
| ### Breaking Changes |
There was a problem hiding this comment.
CHANGELOG vs PR checklist mismatch
The CHANGELOG correctly adds a ### Breaking Changes section for the removal of --sf-animation-delay-1…5, but the PR description checklist is ticked for "No breaking changes." Token removals are semver-breaking regardless of how rarely the tokens were used. The checklist note ("old delay tokens removed, but they were rarely used") is a rationale for shipping the change, not grounds for declaring it non-breaking. Consider un-ticking that item or removing the "No breaking changes" qualifier so the checklist accurately reflects the CHANGELOG.
Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
There was a problem hiding this comment.
Actionable comments posted: 6
🧹 Nitpick comments (1)
optional/utilities.css (1)
142-147: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low valueAlign logical slide classes with physical RTL direction.
The class names
slide-startandslide-endimply a logical inline direction, but the CSStranslateproperty physicalizes the slide along the X-axis. As a result,slide-startslides to the left in both LTR and RTL text directions. Since inline-start is on the right in RTL mode,slide-startshould translate right (positive X) for RTL surfaces.Consider adding directional overrides to ensure correct logical sliding in RTL layouts.
💡 Proposed RTL support
.sf-hover-grow:hover { scale: var(--sf-hover-grow-scale, 1.05); } .sf-hover-shrink:hover { scale: var(--sf-hover-shrink-scale, 0.95); } .sf-hover-float:hover { translate: 0 calc(-1 * var(--sf-hover-lift, 0.25em)); } .sf-hover-sink:hover { translate: 0 var(--sf-hover-lift, 0.25em); } .sf-hover-slide-start:hover { translate: calc(-1 * var(--sf-hover-slide, 0.5em)) 0; } .sf-hover-slide-end:hover { translate: var(--sf-hover-slide, 0.5em) 0; } + + /* Logical RTL overrides */ + :where([dir="rtl"]) .sf-hover-slide-start:hover { translate: var(--sf-hover-slide, 0.5em) 0; } + :where([dir="rtl"]) .sf-hover-slide-end:hover { translate: calc(-1 * var(--sf-hover-slide, 0.5em)) 0; }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@optional/utilities.css` around lines 142 - 147, Update the .sf-hover-slide-start:hover and .sf-hover-slide-end:hover rules to respect document direction: preserve their current translations in LTR and reverse the horizontal translation in RTL using a directional override. Keep the existing --sf-hover-slide value and hover behavior unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@CHANGELOG.md`:
- Around line 11-13: Update the breaking-change migration example in the
changelog to use a concrete fixed duration such as 150ms instead of calc(2 *
var(--sf-stagger-step)); keep token-derived delay guidance separate if it is
still needed.
In `@demos/full-api-demo-with-overrides.html`:
- Around line 1197-1200: Update the .sf-stagger demo tile so the class is
applied to a parent container with multiple child elements, and give each child
the appropriate animation class used by the demo. Preserve the tile structure
and ensure the children visibly demonstrate staggered animation delays.
In `@demos/full-api-demo.html`:
- Around line 1196-1199: Update the .sf-stagger demo tile in full-api-demo.html
so the .sf-stagger container wraps several direct child elements, each using a
time-based animation class such as .sf-fade-in. Preserve the existing tile
structure and label while ensuring the children visibly demonstrate staggered
delays.
In `@docs/demo.html`:
- Around line 1302-1308: The .sf-stagger demo in the motion documentation lacks
a way to trigger its animation when viewed. Update the demo block around the
.sf-stagger list to reuse the existing click-replay pattern or add the
established intersection-trigger hook, while preserving the current staggered
sf-fade-in items.
In `@docs/llm-guide.md`:
- Around line 1039-1042: Expand the authoritative stagger entry in
docs/llm-guide.md around .sf-stagger and --sf-stagger-step to document or link
the sibling-index() implementation path, the eight-child plateau fallback,
reduced-motion gating, and the scroll-driven entrance/exit staggering
limitation. Keep the guidance synchronized with the live token set and clarify
that stagger behavior is bounded and variant-dependent.
In `@docs/motion.md`:
- Around line 73-75: The `.sf-stagger` exclusion is too broad for entrance
animations. Update the guidance at docs/motion.md lines 73-75, docs/motion.md
lines 144-146, and docs/macros.md lines 723-725 to state that staggering has no
effect only for scroll-driven `.sf-entrance--*` animations; preserve the
exclusion for `.sf-exit--*` while noting that `.sf-entrance--*` uses
`.sf-stagger` in its time-based fallback.
---
Nitpick comments:
In `@optional/utilities.css`:
- Around line 142-147: Update the .sf-hover-slide-start:hover and
.sf-hover-slide-end:hover rules to respect document direction: preserve their
current translations in LTR and reverse the horizontal translation in RTL using
a directional override. Keep the existing --sf-hover-slide value and hover
behavior unchanged.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 7cefef17-cf37-4cd3-9ac0-e74df6da6b0f
⛔ Files ignored due to path filters (4)
configurator/src/data/api-index.generated.jsonis excluded by!**/*.generated.*configurator/src/data/classes.generated.jsonis excluded by!**/*.generated.*configurator/src/data/token-registry.generated.jsonis excluded by!**/*.generated.*dist/css-custom-data.jsonis excluded by!**/dist/**
📒 Files selected for processing (26)
CHANGELOG.mdbadges/badge-optimal.jsoncore/motion.csscore/tokens.cssdemos/full-api-demo-with-overrides.htmldemos/full-api-demo.htmldemos/generate.mjsdemos/ultimate-override.cssdocs/api-index.jsondocs/api-index.mddocs/classes.mddocs/demo.htmldocs/llm-guide.mddocs/macros.mddocs/motion.mddocs/registry.jsondocs/test-coverage-6-token-reference.htmldocs/token-annotations.jsondocs/token-index.jsondocs/token-index.mddocs/tokens.mdoptional/utilities.cssreports/full-api-audit/results/tokens-report.jsontests/motion.spec.jstests/token-api.snapshot.jsontoken-registry.json
| /* Stagger — put .sf-stagger on a parent; children get an incrementing | ||
| animation-delay (index × --sf-stagger-step × --sf-motion-scale). */ | ||
| --sf-stagger-step /* per-item delay increment, default 75ms */ | ||
| ``` |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win
Keep the authoritative stagger entry complete.
This entry exposes only the parent/formula/token. Also document or link to the sibling-index() path, the eight-child plateau fallback, reduced-motion gating, and the scroll-driven entrance/exit limitation so LLM-generated usage does not assume every list is unbounded or every entrance variant is staggerable.
As per coding guidelines, docs/llm-guide.md must stay in sync with the live token set and serve as the authoritative LLM reference for the framework API.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@docs/llm-guide.md` around lines 1039 - 1042, Expand the authoritative stagger
entry in docs/llm-guide.md around .sf-stagger and --sf-stagger-step to document
or link the sibling-index() implementation path, the eight-child plateau
fallback, reduced-motion gating, and the scroll-driven entrance/exit staggering
limitation. Keep the guidance synchronized with the live token set and clarify
that stagger behavior is bounded and variant-dependent.
Source: Coding guidelines
…view feedback - configurator: add "stagger" + "hover-" to the motion domain patterns so the new knobs no longer fall through to the Misc bucket (fixes the curation test). - motion.css: give the .sf-stagger calc explicit var() fallbacks (--sf-stagger-step, 75ms / --sf-motion-scale, 1) to match utilities.css and stay self-contained if loaded standalone. - utilities.css: flip .sf-hover-slide-start/-end under [dir=rtl] so the logical start/end names stay correct (translate is physical). - docs: clarify that .sf-stagger pairs with the time-based looping classes and that its effect on .sf-entrance--* is limited to the non-scroll fallback (.sf-exit--* has none); expand the llm-guide stagger entry. - CHANGELOG: use a literal 150ms in the manual-delay migration example. - demos: emit a real multi-child .sf-stagger tile; make the demo.html stagger block click-to-replay. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TsAowhRHNTVVfxvafVtDas
Summary
Replaces the five
--sf-animation-delay-*consumption tokens with a new.sf-staggerutility class and four new hover-transform knob tokens (--sf-hover-grow-scale,--sf-hover-shrink-scale,--sf-hover-lift,--sf-hover-slide), plus a--sf-stagger-stepknob to configure the stagger increment.Motivation: The old delay tokens were a rigid, limited API (only 5 preset steps). The new
.sf-staggerclass is more flexible — it applies an incrementinganimation-delayto every direct child, scaling by--sf-stagger-stepand--sf-motion-scale. This enables arbitrary-length sequences with a single knob. The hover knobs consolidate magic numbers scattered acrossoptional/utilities.cssinto configurable tokens, making hover effects tunable per scope.Changes
Tokens
--sf-animation-delay-1through-5(consumption tokens)--sf-stagger-step(knob,75ms) — per-item delay increment for.sf-stagger--sf-hover-grow-scale(knob,1.05) — scale for.sf-hover-grow--sf-hover-shrink-scale(knob,0.95) — scale for.sf-hover-shrink--sf-hover-lift(knob,0.25em) — translate distance for.sf-hover-float/-sink--sf-hover-slide(knob,0.5em) — translate distance for.sf-hover-slide-start/-endClasses
.sf-stagger(motion class) — applies incrementinganimation-delayto direct children usingsibling-index()(modern) or:nth-child()fallback (8-step plateau)CSS
core/motion.css: New.sf-staggerimplementation with modern and fallback pathsoptional/utilities.css: Hover classes now reference the new knob tokens instead of hardcoded values (with an RTL override for the logical-slide-start/-slide-endnames)core/tokens.css: Removed delay tokens, added stagger and hover knobsDocumentation
docs/motion.md: Updated stagger guidance; removed references to delay tokens; added.sf-staggerusage examplesdocs/macros.md: Added.sf-staggerreference with token table and implementation notesdocs/llm-guide.md: Updated animation section; documented new hover knobs and stagger knobCHANGELOG.md: Added feature entry +### Breaking Changesentry for the token removalTests
tests/motion.spec.js: Added tests for.sf-staggerdelay calculation and knob/motion-scale scalingGenerated artifacts
Type
Checklist
npm run lint:csspassesnpm run buildrebuildsdist/npm testpasses (unit + Playwright e2e)npm run check:versionpasses (no version bump needed)npm run check:llm-guidepasses (LLM guide reviewed and updated)npm run docs,npm run gen:registry,npm run audit)CHANGELOG.mdupdated under## [Unreleased]--sf-animation-delay-1…5is a semver-breaking token removal (documented under### Breaking Changesin the CHANGELOG and indocs/motion.md). Migration: replace hand-indexedanimation-delay: var(--sf-animation-delay-N)with a single.sf-staggerclass on the parent.Notes
The
.sf-staggerclass uses CSSsibling-index()where supported (modern browsers) for unbounded child counts, with a graceful:nth-child()fallback (8 steps, then plateau). It pairs with the time-based looping classes (fade-in/slide-in-*); the scroll-driven.sf-entrance--*/.sf-exit--*path is not staggered (it usesanimation-range).https://claude.ai/code/session_01TsAowhRHNTVVfxvafVtDas