motion.dev v13

Animationsbibliotek · 2,3 kB for animate()

Motion

Én hardware-accelereret animations-API til hele web-platformen — tweens, fjedre, scroll, gestures og view transitions. Alt herunder kører live i din browser.

kB gzip (fuld)
fps lige nu
14demoer
01

Grundlaget: animate()

Én funktion animerer DOM-elementer, CSS-variabler, SVG-attributter, motion values, objekter og rene tal. Transforms (x, scale, rotate) køres uafhængigt af hinanden og hardware-accelereret via WAAPI, hvor browseren tillader det.

animate

Elementer og afspilningskontrol

Alt hvad animate() returnerer kan pauses, spoles, hastighedsjusteres og afventes som et Promise.

t = 0,00 s
Kode
const anim = animate(
  ".box",
  { x: 260, rotate: 180, scale: 1.4, borderRadius: "50%" },
  { duration: 1.6, ease: "easeInOut" }
)

anim.pause()
anim.speed = 0.25
anim.time = 0.8          // spol
await anim               // afvent
anim.cancel()            // tilbage til udgangspunkt
ease

Easing — 8 kurver, samme distance

Indbyggede kurver, egne cubic-bezier, steps() og fjedre som easing. Kurven tegnes med samme easing-funktion som animationen bruger.

Kode
animate(el, { x: 320 }, { duration: 1.4, ease: "easeOut" })
animate(el, { x: 320 }, { duration: 1.4, ease: [0.34, 1.56, 0.64, 1] })
animate(el, { x: 320 }, { duration: 1.4, ease: steps(6) })
animate(el, { x: 320 }, { type: "spring", bounce: 0.4 })
keyframes

Keyframes, times og gentagelser

Flere keyframes pr. værdi, egen fordeling på tidslinjen, repeat med loop, reverse eller mirror — og forskellig transition pr. værdi.

Kode
animate(el, {
  y:       [0, -120, 0, -60, 0],
  rotate:  [0, 90, 180, 270, 360],
  scale:   [1, 1.3, 0.85, 1.15, 1]
}, {
  duration: 2,
  times: [0, 0.25, 0.5, 0.75, 1],
  repeat: Infinity,
  repeatType: "mirror",
  rotate: { ease: "linear" }        // egen transition pr. værdi
})
02

Fjedre der føles fysiske

Fjedre beskrives enten fysisk (stiffness, damping, mass) eller i designtermer (bounce + duration). Motion oversætter fjederen til en lineær easing, browseren kan køre på compositor-tråden.

spring

Fjeder-laboratorium

Kurven herunder samples direkte fra Motions fjeder-generator — altså præcis de værdier, kassen til højre bevæger sig efter.

Kode
animate(el, { x: 300 }, { type: "spring", stiffness: 220, damping: 18, mass: 1 })

// eller i designtermer — varighed og "hvor meget hopper den"
animate(el, { x: 300 }, { type: "spring", duration: 0.8, bounce: 0.4 })

// kurven ovenfor: samme generator, samplet manuelt
const gen = spring({ keyframes: [0, 1], stiffness: 220, damping: 18, mass: 1 })
gen.next(120)   // → { value: 0.63, done: false }  (t i millisekunder)
03

Mange elementer, ét kald

stagger() er en dynamisk delay-funktion: den kan starte fra første, sidste, midten eller et vilkårligt indeks — og selv have sin egen easing.

stagger

Bølge gennem et grid

Kode
animate(
  ".dot",
  { scale: [1, 1.9, 1], backgroundColor: ["#2a2a3d", "#7c5cff", "#2a2a3d"] },
  { duration: 0.7, delay: stagger(0.025, { from: "center", ease: "easeOut" }) }
)
sequence

Koreograferet sekvens

En sekvens er en liste af segmenter. at styrer timingen: et tal er absolut, "<" starter sammen med forrige, "-0.2" overlapper, "+0.3" venter.

Kode
animate([
  [card,     { opacity: [0, 1], y: [40, 0] },        { duration: .5 }],
  [".seq-avatar", { scale: [0, 1], rotate: [-90, 0] }, { type: "spring", at: "-0.25" }],
  [".seq-lines i", { opacity: [0, 1], x: [-24, 0] }, { delay: stagger(0.08), at: "<" }],
  [".seq-badge", { scale: [0, 1.3, 1] },              { at: "+0.1" }]
])
04

Gestures uden event-rod

hover(), press() og inView() håndterer pointer-typer, touch, tastaturfokus og oprydning. Hver returnerer én funktion, der fjerner lytteren igen.

hover · press

Kort med fjeder-respons

press() giver dig også besked, om trykket blev fuldført eller annulleret — og virker med både mus, touch og Enter/mellemrum.

venter …
Kode
hover(el, (element) => {
  animate(element, { scale: 1.06, y: -8 }, { type: "spring", bounce: .4 })
  return () => animate(element, { scale: 1, y: 0 }, { type: "spring" })
})

press(el, (element, startEvent) => {
  animate(element, { scale: 0.94 }, { duration: .12 })
  return (endEvent, { success }) => {
    animate(element, { scale: 1 }, { type: "spring", stiffness: 500, damping: 15 })
    console.log(success ? "gennemført" : "annulleret")
  }
})
05

Motion values — den reaktive kerne

En motion value holder både værdi og hastighed. Den kan afledes (mapValue, transformValue), blødgøres (springValue) og bindes til DOM med styleEffect — uden at du selv rører ved en frame-loop.

motionValue · springValue · mapValue

3D-kort med fjederdæmpet parallakse

To rå værdier følger markøren. De sendes gennem en fjeder, mappes til rotation, glans og dybde, og skrives til DOM én gang pr. frame.

Motion
motion values
x0
spring x0
rotateY
velocity0
Kode
const px = motionValue(0), py = motionValue(0)

const sx = springValue(px, { stiffness: 260, damping: 26 })
const sy = springValue(py, { stiffness: 260, damping: 26 })

const rotateY = mapValue(sx, [-1, 1], ["-14deg", "14deg"])
const rotateX = mapValue(sy, [-1, 1], ["12deg", "-12deg"])
const glare   = transformValue(() => `${50 + sx.get() * 40}% ${50 + sy.get() * 40}%`)

styleEffect(card,  { rotateY, rotateX })
styleEffect(glare, { backgroundPosition: glare })

sx.on("change", v => readout.textContent = v.toFixed(3))
06

Scroll — drevet af browserens egen tidslinje

Hvor ScrollTimeline findes, kobles animationen direkte på scroll uden JavaScript pr. frame. Ellers falder Motion tilbage til en optimeret loop. Samme API begge veje.

scroll

Scroll-låst tidslinje

Bjælken øverst på siden er selve dette API. Herunder er en sektion, hvor et element bevæger sig i takt med, at sektionen passerer viewporten.

0 % af sektionen passeret
Kode
// Sidens progressbar
scroll(animate("#scrollbar", { scaleX: [0, 1] }, { ease: "linear" }))

// Elementets egen tidslinje, målt mod en bestemt sektion
scroll(
  animate("#scrollRunner", { x: ["0%", "100%"], rotate: [0, 360] }, { ease: "linear" }),
  { target: document.querySelector("#scrollTrack"), offset: ["start end", "end start"] }
)

// Eller læs tallene selv
scroll((progress, info) => console.log(progress, info.y.velocity))
scroll

Vandret galleri drevet af lodret scroll

Kode
scroll(
  animate(inner, { x: [0, -(inner.scrollWidth - window.innerWidth)] }, { ease: "linear" }),
  { target: gallery, offset: ["start end", "end end"] }
)
inView

Udløst når elementet ses

Alle overskrifter og kort på denne side kommer ind via inView(). Her kan du se det isoleret — med og uden gentagelse.

01
02
03
04
amount: 0.5
Kode
inView(".iv-item", (element) => {
  animate(element, { opacity: [0, 1], y: [30, 0] }, { duration: .6, ease: "easeOut" })

  // returnér en funktion for at reagere, når elementet forlader viewporten
  return () => animate(element, { opacity: 0.25 })
}, { amount: 0.5, margin: "0px 0px -80px 0px" })
07

SVG, tal og farver

pathLength, pathOffset og pathSpacing normaliseres til 0–1, uanset hvor lang stien er. Og animate() behøver slet ikke et element — den kan interpolere rene tal, farver og komplekse strenge.

pathLength

Streger der tegner sig selv

Kode
animate("path, circle",
  { pathLength: [0, 1], opacity: [0, 1] },
  { duration: 1.6, delay: stagger(0.3), ease: "easeInOut" }
)

// "marching ants"
animate(el, { pathSpacing: 0.5, pathOffset: [0, 1] },
        { duration: 2, repeat: Infinity, ease: "linear" })
tal · farver · arc

Tællere, farveblanding og buer

animate(0, 12480, …) animerer et rent tal. Farver interpoleres i et perceptuelt korrekt rum, og path: arc() sender elementet ad en bue i stedet for en lige linje.

0
#7c5cff
🛒
Kode
animate(0, 12480, {
  duration: 2, ease: "easeOut",
  onUpdate: v => el.textContent = Math.round(v).toLocaleString("da-DK")
})

animate(sw, { backgroundColor: ["#7c5cff", "#12d8b0", "#ff5c8a"] }, { duration: 2 })

animate(item, { x: 240, y: -90 }, { duration: .7, path: arc(), ease: "easeInOut" })
08

Layout- og view transitions

animateView() lægger sig oven på browserens View Transition API: du ændrer bare DOM'en, og Motion animerer forskellen — med fjedre, afbrydelseshåndtering og automatisk matching af delte elementer.

animateView

Grid ↔ liste med delte elementer

Kortene skifter layout fuldstændig. Der er ingen animationskode pr. kort — kun ét kald omkring den DOM-ændring, der skifter klasse.

Kode
animateView(() => {
  list.classList.toggle("grid")        // bare skift DOM'en
}, { type: spring, duration: 0.7, bounce: 0.25 })
  .enter({ opacity: [0, 1], scale: [0.9, 1] })
  .exit({ opacity: 0, scale: 0.9 })
09

Hvad det betyder i praksis

Styrker

  • Én API på tværs af tween, fjeder, scroll, gestures og view transitions
  • Hardware-accelereret, hvor browseren kan — inkl. scroll uden JS pr. frame
  • animate() alene er ~2,3 kB; du betaler kun for det, du importerer
  • Vanilla JS, React, Vue — samme mentale model
  • Respekterer prefers-reduced-motion via MotionGlobalConfig

Ting at være opmærksom på

  • Hele DOM-pakken fylder 135 kB minificeret / 48 kB gzip — kun det, du faktisk importerer, er småt
  • animateView() kræver View Transition API (Chromium i dag) og degraderer ellers
  • I WordPress/Elementor-projekter skal den enqueues som modul for at kunne tree-shakes
  • Layout-animationer er tungere end transforms — brug dem bevidst
npmnpm install motion
ESM / CDNimport { animate } from "https://cdn.jsdelivr.net/npm/motion@13.1.1/+esm"
Selv-hostet<script src="lib/motion.js"></script> → const { animate } = window.Motion