Animation library
Every effect, driver and preset — hover or scroll to play them. No JavaScript.
Browser support
What this page needs
animation-timeline: view() / scroll()Drives
.animate-view,.animate-scroll, every journey,.text-revealand.scroll-size. Deliberately not polyfilled — see below.sibling-index()Gives
.staggerits per-child delay with no JavaScript. Without it the delay stays 0 and staggered items animate together — a degraded effect, not a broken one.offset-pathThe
.float-path-*presets. The waypoint floats (.float-*) are plain keyframes and work everywhere.linear() easingThe multi-stop float easings (
--ease-float-sine,--ease-float-bounce). Falls back to the declared timing function.
How the framework handles what's missing
Scroll-driven animations are not polyfilled
A deliberate choice, and the one exception to the polyfill strategy. A scroll-timeline polyfill is large and runs on every frame, to buy back an effect that is decorative by definition — so instead animation.css asks the engine directly and cancels the affected animations outright:
@supports not (animation-timeline: view()) {
:where(.animate-view, .animate-scroll) { animation: none }
}Without that cancel these elements would sit at their first keyframe forever — and an entrance animation that never runs is opacity: 0, i.e. invisible content. Cancelling costs an animation; not cancelling costs the page.
There is no class and no inline snippet to install: the check is CSS, so it holds on a page that never loads a byte of JavaScript. The same reasoning governs.animate-trigger, which is paused only inside@media (scripting: enabled) — pausing is safe only when something can un-pause it.
What is polyfilled
Shipped as feature-detected, self-hosted ES-module chunks (@getvitops/core/polyfills): each is fetched only when its test fails, so a current browser downloads none of them.
anchor-name / position-anchorPolyfilled via
@oddbird/css-anchor-positioning, loaded after render.[popover] / popovertargetPolyfilled via
@oddbird/popover-polyfill. The theme editor panel is a popover.<template shadowrootmode>Hydrated by
@webcomponents/template-shadowrootwhen the parser doesn’t.adoptedStyleSheetsPolyfilled via
construct-style-sheets-polyfill.TemporalPolyfilled via
temporal-polyfill.
Not polyfilled because they're universal across the framework's targets:<dialog>, :has(), container queries, CSS nesting,IntersectionObserver. Invoker Commands (command /commandfor) is not yet polyfilled or relied on.
The model
An effect carries no motion of its own — it's a pure value layer, setting--<prop>-from / -to and an --_anim keyframe name. Adriver supplies the motion. You always compose one of each.
Keyframe drivers
.animate-view— plays as the element enters the viewport.animate-scroll— scrubs against page scroll.animate-trigger— plays once, on demand
Scroll-driven, via animation-timeline. Where that's unsupported,@supports cancels the animation so content renders in its natural visible state rather than stuck at opacity: 0.
Transition driver
.transition+hover-<fx>.transition+focus-<fx>.transition+active-<fx>
Reversible and interruptible. Different effects can bind to different states on one element:.transition.hover-slide-up.active-elevate-up.
Everything below respects prefers-reduced-motion: reduce — keyframes, transitions and floats are all dropped, and elements settle at their resolved state.
Effects
Hover each tile to play it. Grouped by the keyframe family they drive —composite (transform/opacity, cheapest), paint (filter/clip) andlayout (size, the only one that reflows).
The dashed outline is where the element lands; the block inside it is the element. They start apart on purpose — a hover-<fx> variant's resting state is the effect's from endpoint, so hover-fade-in genuinely sits atopacity: 0 until you hover it. The label lives on the outline so it stays readable while the block is faded, clipped or displaced.
Hovering the outline works too. Each state variant matches the elementor its direct parent (.hover-<fx>:hover, :hover > .hover-<fx>), which is what makes reveal-* usable at all: those rest atclip-path: inset(0 100% 0 0), and clip-path clipshit-testing as well as painting — so an element clipped to nothing can never receive the hover that would reveal it. The parent is the handle.
composite 10
fade-infade-outslide-upslide-downslide-leftslide-rightscale-upscale-downrotate-cwrotate-ccwpaint 8
blur-inblur-outelevate-upelevate-downreveal-leftreveal-rightreveal-upreveal-downlayout 2
The only stage that reflows, and the only one behind a feature gate:height: 0 → auto needs interpolate-size: allow-keywords to be interpolable at all. Where that's missing the tile snaps instead of growing — thelayout keyframe has the same dependency, so this isn't a weakness of the transition driver.
autosize-growautosize-shrinkScroll-driven
The same effects under .animate-view. Scroll up and back down — they replay, because the timeline is the scroll position, not a one-shot.
Timed off the element's midpoint: motion starts once that midpoint is 10% of the viewport in and completes at 25%. Both stops are the element's position on screen, not a fraction of its own height — so these tiles and a full-bleed section start at the same moment. The pivot is entry 50%, which is exactly where a midpoint meets the viewport edge whatever the element measures; --anim-start / --anim-end move the window, --anim-range replaces it.
fade-inslide-upslide-leftscale-upblur-inreveal-leftJourneys
Compositions of the base parts (fade, slide, scale,blur) into one keyframe, so multiple properties move on a single timeline rather than fighting over animation-name. A journey is entry → hold → exit, so it needs a longer range than a one-shot entrance: it starts on the same 10% midpoint pivot but runs to the end of the exit phase, putting the hold in the middle of the crossing rather than at the bottom edge of the screen.
fade-journeyslide-journeyscale-journeyblur-journeyfade-slide-journeyfade-scale-journeyfade-blur-journeyfade-slide-scale-journeyfade-scale-blur-journeyStagger
.stagger on the parent offsets each child's animation-delay by--stagger-amount. Pure CSS via sibling-index() where supported, with a JS fallback that sets the index inline.
animation-delay is time-based and is ignored on a scroll-driven timeline, so .stagger offsets each child's view animation-range by the same index as well — one class, both driver families. Journeys declare their own range and so opt out; tune the scroll-driven step with --stagger-range-step.
Floats
Ambient, infinite motion — waypoint presets over a shared float keyframe. Tune with--float-radius, --float-distance and --float-duration.
float-circularfloat-circular-ccwfloat-figure-8float-bounce-yfloat-drift-xfloat-gentlefloat-organicSpeed modifiers
float-slowfloat-fastfloat-linearOffset paths
float-path-circlefloat-path-ellipseText effects
Typographic motion from patterns/text-effects.css, plus the scroll-driven.text-reveal two-tone fill.
.text-gradient-animateDesign systems in motion
.text-highlightDesign systems in motion
.text-rotateDesign systems in motion
.text-staggerDesign systems in motion
.text-flipDesign systems in motion
.typingDesign systems in motion
.text-revealScroll to fill this line from one colour to the other