Article — "CSS Without JavaScript: animation-trigger, Scroll-Driven Carousels, and Declarative Interaction Primitives": Article — "CSS Without JavaScript: animation-trigger, Scroll-Driven Carousels, and Declarative Interaction Primitives": Two facts sit uncomfortably next to each other in the CSS-Tricks archive, reportedly around this period.
One: jhey posts four lines of CSS that produce a working carousel micro-interaction, and it reportedly goes past 27,000 views.
Two: the navigation-matching spec, a sibling primitive meant to make view transitions declarative, is still renaming its own keywords — `@route` reportedly became `@location`, a change that appears to have happened since Bramus's post.
The first fact reads as arrival. The second reads as a feature still deciding what to call itself. Most readers treat these as unrelated: one primitive is ready, another isn't. That's the wrong frame. Both are telling you the same thing about what these primitives actually cost.
Start with jhey's four lines, because you should feel the win before you distrust it:
`li { view-timeline: --item inline } li::scroll-marker { animation: grow linear both; animation-timeline: --item; }`
That's a carousel with dot indicators that grow as each item scrolls into view. "Progressive enhancement for drag" comes as an aside. This used to be a JavaScript library, an Intersection Observer wired to compute scroll progress, and an accessibility bug tracker you eventually stopped opening.
Now it's a declaration.
Last week in this space, the argument was that dependencies are becoming redundant — that you can ship less JavaScript because the platform finally covers what libraries used to. This week the argument sharpens: the replacements aren't lighter versions of the old job. They're a different job.
The replacements aren't lighter versions of the old job. They're a different job — one that inverts the model from describing how to declaring what.
The redundancy is real, and worth naming precisely. Per SitePoint, Swiper v11's full minified bundle runs about 140KB; even modular imports land 40–60KB for core plus navigation. That's the number you delete.
But the more interesting deletion is the bug class. JavaScript carousels "routinely broke keyboard navigation, failed to expose proper ARIA semantics, and hijacked focus in unpredictable ways." Only the most diligent library authors patched in screen reader support. Everyone else ignored it.
The CSS carousel reportedly gives you keyboard and ARIA semantics for free — not because someone was diligent, but because `::scroll-marker` and `::scroll-button` are semantic pseudo-elements the browser understands as navigation. The accessibility isn't a feature you remembered to add. It's a property of the primitive.
The same pattern shows up in MicroLighter, which ships syntax highlighting on the `::highlight()` pseudo — per CSS-Tricks, Baseline this year — instead of the spans, classes, and bloated JavaScript that Prism-style highlighters require. It reportedly brings `light-dark()` and custom properties along for free. Themes become `--syntax-keyword: light-dark(#cf222e, #ff7b72)`, not a config object.
So the win is genuine. Here's the rug pull: none of this is a drop-in replacement, and pretending it is will hurt you.
Look at what jhey's four lines assume you already know. `view-timeline: --item inline` names a timeline. `animation-timeline: --item` binds an animation to it. If you don't already hold "a timeline is a named progress axis you attach animations to" in your head, those four lines are not four lines — they're a vocabulary you haven't been taught.
That's the discontinuity. The old model was imperative: observe the scroll event, compute how far along you are, mutate the DOM in response. You described *how* the interaction unfolds, step by step, and you could debug it by logging each step.
The new model asks you to declare *what state maps to what visual* and hand the unfolding to the engine. There's no step to log. There's a mapping to get right.
Nowhere is this clearer than `animation-trigger`, which does something that breaks imperative intuition entirely: it separates the trigger from the thing that animates.
You put `timeline-trigger: --trigger scroll() contain / cover` on one element. You put `animation-trigger: --trigger play` on a completely different element. "Triggers and animations don't have to be on the same element." When the trigger element enters view, the animated elements fire.
In JavaScript you'd have wired that with a callback — one element listens, another reacts, and the wire between them is code you wrote. Here the wire is a shared name in the global scope, resolved by the cascade. Per CSS-Tricks, define the same trigger name twice and the later one wins. That's not a bug you debug with a breakpoint; it's a cascade rule you have to already understand.
And then there's `contain / cover`. That reads like decoration until you learn it isn't. `contain` is the activation range — the trigger turns on when the element is fully in the scrollport. `cover` is the active range — the trigger stays on as long as any part remains visible.
The spec, per CSS-Tricks, has a rule buried in it: the active range must include the activation range, or the trigger can never turn on. Get that relationship backwards and nothing fires, silently. There's no error. There's no console. There's a mapping you held wrong.
This is the maturity gap that actually matters, and it's not the one everyone frets about.
The browser-support conversation is loud: scroll-driven animations have broad but not universal engine support, Firefox is partial as of mid-2026, and you're told to wrap everything in `@supports` blocks with a scrollable-container fallback. Real, but manageable — progressive enhancement is a solved discipline. jhey's own note that the carousel "falls back to `<button>`" is the whole strategy in three words.
The dangerous gap is the one between a battle-tested imperative library and a declarative primitive whose activation-range semantics live entirely in your head. Swiper has a decade of edge cases beaten out of it. When it misbehaves, you can step through it.
When your `animation-trigger` doesn't fire, you're not debugging code. You're re-reading a spec to check whether `contain / cover` means what you thought, and discovering the spec draft is "vague on this at the moment" — which is exactly what CSS-Tricks says about the `at` keyword in navigation-matching.
Which brings back the two uncomfortable facts. `@route` becoming `@location` isn't cosmetic churn — it's the signal that the grammar of these primitives is still being written while people are trying to learn it. Preethi's frustration in the CSS-Tricks Slack lands here: there's `@property`, `@color-profile`, `@position-try`, and now `@location`, each a separate at-rule to memorize. "Anything that lowers the learning curve is certainly worth considering."
The learning curve is the story. Not the support matrix.
Jake Dohm's question under jhey's post — "Is there a modern carousel component/framework based on all this new tech?" — is the tell. The instinct is still to find a library that wraps the primitive, because a library is where the mental model gets hidden. But the whole point of the primitive is that there's no *how* to hide anymore. There's only the *what*, and you own it.
So here's the honest version of the workflow gain. The 140KB goes away. The accessibility comes for free. The four lines are real.
But the four lines only work for a designer who has relearned the grammar — timelines as named axes, triggers as scoped names resolved by the cascade, activation ranges nested inside active ranges. Carry the imperative model into this and you'll write CSS that looks declarative and debug it like it's code, and it will fight you the whole way.
The primitives didn't make the job lighter. They inverted it. The people fretting about Firefox are worried about the wrong quarter.