Animation library

Every effect, driver and preset — hover or scroll to play them. No JavaScript.

On this page

Browser support

What this page needs

  • animation-timeline: view() / scroll()

    Drives .animate-view, .animate-scroll, every journey, .text-reveal and .scroll-size. Deliberately not polyfilled — see below.

  • sibling-index()

    Gives .stagger its 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-path

    The .float-path-* presets. The waypoint floats (.float-*) are plain keyframes and work everywhere.

  • linear() easing

    The 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-anchor

    Polyfilled via @oddbird/css-anchor-positioning, loaded after render.

  • [popover] / popovertarget

    Polyfilled via @oddbird/popover-polyfill. The theme editor panel is a popover.

  • <template shadowrootmode>

    Hydrated by @webcomponents/template-shadowroot when the parser doesn’t.

  • adoptedStyleSheets

    Polyfilled via construct-style-sheets-polyfill.

  • Temporal

    Polyfilled 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-in
fade-out
slide-up
slide-down
slide-left
slide-right
scale-up
scale-down
rotate-cw
rotate-ccw

paint 8

blur-in
blur-out
elevate-up
elevate-down
reveal-left
reveal-right
reveal-up
reveal-down

layout 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.

auto
size-grow
auto
size-shrink

Scroll-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-in
slide-up
slide-left
scale-up
blur-in
reveal-left

Journeys

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-journey
slide-journey
scale-journey
blur-journey
fade-slide-journey
fade-scale-journey
fade-blur-journey
fade-slide-scale-journey
fade-scale-blur-journey

Stagger

.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.

1
2
3
4
5
6

Floats

Ambient, infinite motion — waypoint presets over a shared float keyframe. Tune with--float-radius, --float-distance and --float-duration.

float-circular
float-circular-ccw
float-figure-8
float-bounce-y
float-drift-x
float-gentle
float-organic

Speed modifiers

float-slow
float-fast
float-linear

Offset paths

float-path-circle
float-path-ellipse

Text effects

Typographic motion from patterns/text-effects.css, plus the scroll-driven.text-reveal two-tone fill.

.text-gradient-animate

Design systems in motion

.text-highlight

Design systems in motion

.text-rotate

Design systems in motion

.text-stagger

Design systems in motion

.text-flip

Design systems in motion

.typing

Design systems in motion

.text-reveal

Scroll to fill this line from one colour to the other