skip to content

How would you ship a scroll-driven CSS animation on a production site knowing some visitors' browsers do not support animation-timeline?

level: principalimportance: should knowfreq 26%

answer

  1. decorative, therefore optional
  2. base styles are the end state
  3. @supports tests property and value
  4. never hide content in the base rule
  5. polyfilling reintroduces the cost

basics

~10 s

Author the finished, static state as the base style and layer the animation inside @supports (animation-timeline: view()). Unsupported browsers get the correct end state with no motion, and no fallback script is needed.

solid answer

~50 s

Treat it as pure progressive enhancement rather than a feature to polyfill. The base rule holds the state you want when nothing runs — a reveal's base is fully visible, not `opacity: 0` — and the animation goes inside `@supports (animation-timeline: view())`, which tests the property-and-value pair rather than a browser. That inversion is the whole trick: the common mistake is hiding elements in the base rule and revealing them only through the animation, which turns an unsupported browser into a blank page. The feature degrades cleanly because an unsupported `animation-timeline` is simply an invalid declaration, and a browser that supports it but ends up with an inactive timeline runs nothing at all. Chromium shipped it in Chrome 115 in 2023 with other engines following, so decide from your own analytics whether the enhanced path is the majority experience or a bonus.

code

css · 15 lines
css
@keyframes fade-up {
  from { opacity: 0; transform: translateY(24px); }
  to   { opacity: 1; transform: none; }
}

/* Base state = the finished result. */
.reveal { opacity: 1; transform: none; }

@supports (animation-timeline: view()) {
  .reveal {
    animation: fade-up linear both;
    animation-timeline: view();
    animation-range: entry 0% entry 100%;
  }
}

go deeper

for a junior

Know that @supports (animation-timeline: view()) guards a rule for browsers that understand the feature, and that the styles outside the block are what everyone else sees.

for a middle

Explain why the base state must be the finished result rather than the hidden start, and that @supports tests a property-and-value pair because support is per value.

for a senior

Show how you would verify the fallback — disable the block and review the page — and connect it to the inactive-timeline cases that fail the same way even in a supporting browser.

for a principal

Own the decision itself: state the rule that scroll-linked motion may never carry meaning, refuse to polyfill decoration on cost grounds, and set the baseline from real support data rather than from what runs on your machine.

## Frame the decision, not just the syntax The interesting part of this question is not `@supports` — it is deciding what "no animation" should look like and making that the default. Scroll-driven animation is decorative by nature: it tells the reader where they are and draws the eye. That means the content must be complete and usable without it, and the enhancement must be additive. ## Invert the base state The single highest-value rule is: **the base styles are the end state.** ```css /* Base: what an unsupported browser gets — the finished result. */ .reveal { opacity: 1; transform: none; } @supports (animation-timeline: view()) { .reveal { animation: fade-up linear both; animation-timeline: view(); animation-range: entry 0% entry 100%; } } ``` The failure this prevents is the one that actually ships: putting `opacity: 0` on `.reveal` and relying on the animation to bring it back. In a browser without support, the `animation-timeline` declaration is invalid and dropped, the animation either does not exist or runs instantly on the document timeline, and the reader may be left staring at invisible content. The same hazard exists even in a supporting browser whenever the timeline turns out to be inactive — a scroller with no overflow on the chosen axis, or an unresolvable timeline name — because an animation on an inactive timeline never progresses. ## Why @supports and not a user-agent check `@supports (animation-timeline: view())` asks the parser whether it understands that property with that value. It is the only reliable test, it needs no maintenance as engines ship the feature, and it costs nothing at runtime. Test the exact value you intend to use: `scroll()` and `view()` are different values of the same property, and testing a property name alone with no value is not a meaningful check. Avoid the alternatives. Version sniffing goes stale immediately. A JavaScript polyfill that reattaches scroll listeners reintroduces exactly the main-thread work the feature exists to remove, and on a decorative effect that is a poor trade — it costs every visitor bytes and frames so that a shrinking minority sees a flourish. ## What to enhance and what never to Draw the line at meaning. Anything that conveys state or content — a value the reader needs, a control's affordance, whether something is available — must not depend on scroll position. A progress bar that is the *only* indication of position is fine to omit entirely; a menu that only becomes reachable once an animation has run is a defect. Ask of every scroll-driven effect: if this never runs, is anything lost besides polish? If the answer is yes, it is not an enhancement, and it belongs in a mechanism that always works. ## Support and how it moves Chromium shipped scroll-driven animations in Chrome 115 in July 2023, with other engines adding support subsequently, so the enhanced path is now the majority experience for most audiences — but that is exactly the sort of claim to verify against current support data and your own analytics rather than assert from memory. The right posture for a long-lived codebase is that the `@supports` block is not temporary scaffolding to delete once support is universal; it is a permanent statement that this effect is optional, and it keeps working as a safety net for the inactive-timeline cases that no amount of browser support removes. ## Layered defence in practice A production-grade setup usually has three parts: 1. **A correct static base**, authored first and reviewed with the animation deliberately disabled. 2. **The `@supports` block**, testing the exact property-value pair used inside it. 3. **A deliberate `animation-fill-mode`**, normally `both`, so that even inside a supporting browser an element that has passed its range does not snap back to a state you did not intend. A useful review ritual: comment out the `@supports` block and load the page. If it looks finished, the enhancement is safe to ship. If anything is invisible, unreachable, or mid-transition, the base state is wrong — fix that before touching the animation. ## The judgment an interviewer is listening for They want to hear that you separate the *decision* (is this decorative? what is the floor?) from the *mechanism* (`@supports`), that you refuse to polyfill decoration, and that you have a concrete way to verify the fallback rather than assuming it. Candidates who jump straight to a feature-detection snippet without saying what the page looks like when the animation never runs have answered the smaller half of the question.

  • What goes wrong if the base rule sets opacity: 0 and the animation is expected to reveal it?
    In a browser without support the animation-timeline declaration is dropped, so nothing scrolls the element back into view and the content can stay invisible. The same happens in a supporting browser whenever the timeline turns out to be inactive. Author the visible state as the base and let the enhancement start from hidden inside the keyframes instead.
  • Why test @supports (animation-timeline: view()) rather than the property name alone?
    Because support is per value, not per property, and a bare property test is not a meaningful check of what you are about to use. `scroll()` and `view()` are distinct values, so test the exact pair the block depends on. It also keeps the guard honest if you later change which timeline function the rule uses.
  • Would you polyfill scroll-driven animations with a scroll listener for unsupported browsers?
    Generally no. The effect is decorative, and the polyfill reintroduces per-frame main-thread work for every visitor — the exact cost the CSS feature exists to avoid — to deliver polish to a shrinking minority. Ship the static base instead, and spend the budget on something the reader would actually miss.
  • How do you verify the fallback path without an unsupported browser to hand?
    Comment out the `@supports` block, or temporarily change its condition to something no engine supports, and load the page. If it reads as finished and everything is reachable, the base state is correct. Making that a review step catches inverted base states long before they reach an unsupported browser.

saying these in an interview costs you the question

  • Hides content in the base rule and reveals it only via the animation
  • Feature-detects by sniffing the browser version instead of @supports
  • Adds a scroll-listener polyfill for a purely decorative effect
  • Assumes universal support because it works in their own browser
  • Treats the @supports block as temporary scaffolding to delete later

context