Skip to content

Motion — Overview

@andersseen/motion is a separate package from web-components — a framework-agnostic animation library: attribute-driven CSS animations for declarative use, plus an imperative promise-based player for component-level transitions (modal open/close, toast enter/exit, etc.). Zero in-repo dependencies.

Part of the Andersseen product core — usable entirely on its own, in plain HTML or any framework, without @andersseen/web-components. It’s also what @andersseen/web-components and @andersseen/vanilla-components use internally for the animated prop on components like Modal and Toast.

Terminal window
pnpm add @andersseen/motion
@import '@andersseen/motion/style.css';

Scan the DOM once (or after an SPA route change) and every [and-motion] element gets wired up automatically:

import { initMotion } from '@andersseen/motion';
const cleanup = initMotion();
// …later, on unmount / route change:
cleanup();
Hover me (pulse)
Press me (tada)
<!-- Animate once when scrolled into view -->
<div and-motion="fade-in" and-motion-trigger="enter">Content</div>
<!-- Animate on hover -->
<div and-motion="zoom-in" and-motion-trigger="hover">Hover me</div>
<!-- Custom timing -->
<div
and-motion="slide-in-up"
and-motion-trigger="enter"
and-motion-duration="800ms"
and-motion-delay="200ms"
>
Delayed slide
</div>
AttributeValuesDefaultNotes
and-motionanimation name (below)required
and-motion-triggerenter | hover | tapinferrednames containing -in default to enter if omitted
and-motion-durationany CSS <time>stylesheet defaultforwarded to --and-motion-duration
and-motion-delayany CSS <time>0sforwarded to --and-motion-delay
and-motion-easingany CSS easingstylesheet defaultforwarded to --and-motion-easing
and-motion-oncetrue | falsecontroller default (true)per-element override of “animate only the first time”
and-motion-stateset to active internally while the animation is running; read-only

enter uses IntersectionObserver (threshold/rootMargin/once are configurable via initMotion({ threshold, rootMargin, once })). hover listens for mouseenter/mouseleave. tap listens for pointerdown/pointerup/pointercancel/pointerleave.

prefers-reduced-motion: reduce is respected at both the CSS layer (via @media) and the JS layer (enter-triggered elements skip the initial opacity: 0 so nothing is ever stuck invisible).

Every timing value is a custom property, overridable at any scope (:root for a site-wide default, a component wrapper for a scoped default, or per-element via and-motion-duration/and-motion-delay/and-motion-easing as shown above).

PropertyDefaultUsed by
--and-motion-duration500msevery animation’s animation-duration
--and-motion-delay0msevery animation’s animation-delay
--and-motion-easingcubic-bezier(0.16, 1, 0.3, 1)every animation’s animation-timing-function
--and-motion-easing-entercubic-bezier(0.16, 1, 0.3, 1)not wired to a selector — reference token for your own entrance CSS
--and-motion-easing-exitcubic-bezier(0.7, 0, 0.84, 0)reference token for your own exit CSS
--and-motion-easing-springcubic-bezier(0.34, 1.56, 0.64, 1)reference token, playful overshoot
--and-motion-easing-smoothcubic-bezier(0.25, 0.1, 0.25, 1)reference token, general smooth
--and-motion-easing-bouncecubic-bezier(0.175, 0.885, 0.32, 1.275)reference token
--and-motion-distance20pxsmall directional offset (fade-up, slide-left, …)
--and-motion-distance-big150px-big fading variants (fade-down-big, …)
--and-motion-scale-from0.95reference token (not currently wired to a keyframe)
--and-motion-scale-to1reference token

Only --and-motion-duration, --and-motion-delay, and --and-motion-easing are actually forwarded by the and-motion-* attributes — the -enter/-exit/ -spring/-smooth/-bounce easing variants and the scale tokens exist so your own custom keyframes (or a createMotionPlayer call — see below) can reference the same design-token vocabulary instead of hardcoding cubic-bezier(...) values inline.

/* Site-wide: snappier, no delay */
:root {
--and-motion-duration: 250ms;
}
/* Scoped: this card's entrances use the spring easing token */
.pricing-card [and-motion] {
--and-motion-easing: var(--and-motion-easing-spring);
}

initMotion() is a thin wrapper around new MotionController(options) — reach for the class directly when you need to re-scan after injecting new [and-motion] elements into the DOM (e.g. after an innerHTML update or a non-reactive template render that initMotion’s one-shot scan won’t see):

import { MotionController } from '@andersseen/motion';
const controller = new MotionController({ threshold: 0.2, once: true });
// … later, after adding new [and-motion] nodes to the page:
controller.scan(); // idempotent — already-wired elements are skipped
// … on unmount:
controller.destroy();
OptionTypeDefaultNotes
rootHTMLElementdocument.bodyOnly elements inside this root are scanned.
thresholdnumber0.1IntersectionObserver threshold for enter triggers.
rootMarginstring'0px'IntersectionObserver root margin for enter triggers.
oncebooleantrueController-level default for “animate only the first time”. Per-element and-motion-once overrides it.

scan() and destroy() are the only two public methods beyond the constructor — initMotion() just calls new MotionController(options) and returns () => controller.destroy(), so the two are interchangeable; use the class when you need scan().

Motion has no framework runtime dependency — call initMotion() on mount and its returned cleanup function on unmount:

// React
import { useEffect } from 'react';
import { initMotion } from '@andersseen/motion';
useEffect(() => {
const cleanup = initMotion();
return cleanup;
}, []);
<!-- Vue -->
<script setup lang="ts">
import { onMounted, onUnmounted } from 'vue';
import { initMotion } from '@andersseen/motion';
let cleanup: () => void;
onMounted(() => {
cleanup = initMotion();
});
onUnmounted(() => cleanup?.());
</script>
// Angular
import { AfterViewInit, Component, OnDestroy } from '@angular/core';
import { initMotion } from '@andersseen/motion';
@Component({ selector: 'app-root', template: `...` })
export class AppComponent implements AfterViewInit, OnDestroy {
private cleanup?: () => void;
ngAfterViewInit() {
this.cleanup = initMotion();
}
ngOnDestroy() {
this.cleanup?.();
}
}

In Astro, React, or Vue apps rendered server-side, call initMotion() only in a client-side context (a plain <script> in Astro, a useEffect/onMounted hook elsewhere) — it touches document and IntersectionObserver, both unavailable during SSR.

@andersseen/web-components reads a single global flag to decide whether components like Modal, Toast, Dropdown, and Tooltip animate their open/close transitions by default — instead of setting animated on every instance individually, call this once at app bootstrap:

import { enableAnimations } from '@andersseen/web-components';
enableAnimations(); // every component below now defaults to animated="true"
FunctionDescription
enableAnimations()Sets the global flag; components read it in componentWillLoad().
disableAnimations()Clears it (the library default — nothing animates unless opted in).
isAnimationsEnabled()boolean — current state of the flag.

This is orthogonal to prefers-reduced-motion, which always wins regardless of this flag — it’s a product decision (“do we want motion in this app at all”), not an accessibility override.

Complete, verified-against-source list of every and-motion="<name>" value — 135 in total. Each family’s “clean” base name (no -in-/-out- infix) is an alias for its entrance form; use the explicit -in-/-out- name when you want the direction unambiguous in markup, or the exit name to trigger the reverse. All of these also work with createMotionPlayer.

DirectionEntranceExit
nonefade / fade-infade-out
downfade-down / fade-in-downfade-out-down
down (big)fade-down-big / fade-in-down-bigfade-out-down-big
leftfade-left / fade-in-leftfade-out-left
left (big)fade-left-big / fade-in-left-bigfade-out-left-big
rightfade-right / fade-in-rightfade-out-right
right (big)fade-right-big / fade-in-right-bigfade-out-right-big
upfade-up / fade-in-upfade-out-up
up (big)fade-up-big / fade-in-up-bigfade-out-up-big
top-leftfade-top-left / fade-in-top-left— (no exit variant)
top-rightfade-top-right / fade-in-top-right— (no exit variant)
bottom-leftfade-bottom-left / fade-in-bottom-left— (no exit variant)
bottom-rightfade-bottom-right / fade-in-bottom-right— (no exit variant)

Cardinal directions only, opacity + transform (no scale, unlike fading).

DirectionEntranceExit
upslide-up / slide-in-upslide-out-up
downslide-down / slide-in-downslide-out-down
leftslide-left / slide-in-leftslide-out-left
rightslide-right / slide-in-rightslide-out-right
DirectionEntranceExit
nonezoom / zoom-inzoom-out
downzoom-down / zoom-in-downzoom-out-down
leftzoom-left / zoom-in-leftzoom-out-left
rightzoom-right / zoom-in-rightzoom-out-right
upzoom-up / zoom-in-upzoom-out-up

Bare bounce is an attention seeker (a one-shot wiggle in place), not an entrance — directional names only here.

DirectionEntranceExit
nonebounce-inbounce-out
downbounce-down / bounce-in-downbounce-out-down
leftbounce-left / bounce-in-leftbounce-out-left
rightbounce-right / bounce-in-rightbounce-out-right
upbounce-up / bounce-in-upbounce-out-up

3D-perspective rotation. backface-visibility: visible is applied automatically so the flipping face doesn’t disappear mid-rotation.

AxisEntranceExit
none (Y, 360°)flip
Xflip-x / flip-in-xflip-out-x
Yflip-y / flip-in-yflip-out-y
DirectionEntranceExit
nonerotate / rotate-inrotate-out
down-leftrotate-down-left / rotate-in-down-leftrotate-out-down-left
down-rightrotate-down-right / rotate-in-down-rightrotate-out-down-right
up-leftrotate-up-left / rotate-in-up-leftrotate-out-up-left
up-rightrotate-up-right / rotate-in-up-rightrotate-out-up-right

Overshoots past scale(1) then settles — a “pulled back and released” feel.

DirectionEntranceExit
downback-down / back-in-downback-out-down
leftback-left / back-in-leftback-out-left
rightback-right / back-in-rightback-out-right
upback-up / back-in-upback-out-up

Skewed slide with an overshoot settle — no -big or diagonal variants.

DirectionEntranceExit
leftlight-speed-left / light-speed-in-leftlight-speed-out-left
rightlight-speed-right / light-speed-in-rightlight-speed-out-right

One-shot effects with no entrance/exit split — play them on an already-visible element (e.g. and-motion-trigger="hover" or via createMotionPlayer) rather than on scroll-in.

Attention seekers: pulse, rubber-band, shake-x, shake-y, head-shake, swing, tada, wobble, jello, heart-beat, flash, bounce, scale-up, scale-down.

Specials: hinge (2s, swings and drops off — a “detach and fall” effect), jack-in-the-box, roll / roll-in, roll-out.