Skip to content

Rule · Smoothness

Content is invisible without scroll-driven-animation support

smooth/scroll-timeline-no-fallbackerrorvoid smoothupdated

Why it matters

animation-timeline: view()/scroll(), view-timeline and animation-range have no fallback path: a browser that doesn't implement them (Firefox, at every version through 151) evaluates the enclosing @supports (animation-timeline: …) block as false and drops every rule inside it — including the keyframes that would ever make the element visible. If the element's base rule (outside that block) sets opacity: 0, visibility: hidden, a collapsed scale/mask or an off-screen translate, the content isn't degraded, it's gone — permanently, not just until scrolled into view.

How to fix it

Keyframes own the from-state: the resting cascade (everything outside @supports (animation-timeline: …)) must already be the finished, fully visible state, the way @utility reveal in packages/tokens/css/base.css does it — opacity: 0 lives only inside @keyframes, never in the base rule. Move the hidden state into the keyframe referenced from inside the @supports block; see skills/motion/references/fallbacks.md.

Example

css
/* wrong: base rule hides it unconditionally, only @supports undoes it */
.hero-sub { opacity: 0; }
@supports (animation-timeline: view()) {
  .hero-sub { animation: reveal linear both; animation-timeline: view(); }
}

/* right: resting state is already visible; the keyframe owns opacity: 0 */
@supports (animation-timeline: view()) {
  .hero-sub { animation: reveal linear both; animation-timeline: view(); }
}
@keyframes reveal { from { opacity: 0; translate: 0 0.75rem; } }

References

void smooth reports 13 rules in this category. How the page feels after it loads: dropped frames during a scripted scroll, long animation frames, INP of real clicks, shifts after input, animated layout properties, idle rAF loops and reduced-motion handling.