Animation
Animate any value with the motion component, from simple prop changes to orchestrated sequences across whole trees.
motion-sv gives you several ways to animate your UI, from a single prop that follows your state to orchestrated animations that run through a whole tree of elements.
Basic animations
Almost every animation runs on a motion component. It's a normal DOM element with animation superpowers:
<script lang="ts">
import { motion } from "motion-sv";
</script>
<motion.div /> For basic animations, set values on the animate prop:
<motion.div animate={{ opacity: 1 }} /> Whenever a value in animate changes, the component animates to it. Because animate is a normal prop, it can read Svelte state, and the element follows that state:
<script lang="ts">
let x = $state(0);
</script>
<motion.div animate={{ x }} />
<input type="range" min={-200} max={200} bind:value={x} /> <script lang="ts">
import { motion } from "motion-sv";
let x = $state(0);
let y = $state(0);
let rotate = $state(0);
</script>
<div class="example">
<div>
<motion.div class="box" animate={{ x, y, rotate }} transition={{ type: "spring" }} />
</div>
<div class="inputs">
<label>
<code>x</code>
<input type="range" min={-200} max={200} bind:value={x} />
<input type="number" min={-200} max={200} bind:value={x} />
</label>
<label>
<code>y</code>
<input type="range" min={-200} max={200} bind:value={y} />
<input type="number" min={-200} max={200} bind:value={y} />
</label>
<label>
<code>rotate</code>
<input type="range" min={-180} max={180} bind:value={rotate} />
<input type="number" min={-180} max={180} bind:value={rotate} />
</label>
</div>
</div>
<style>
.example {
display: flex;
align-items: center;
}
.example :global(.box) {
width: 200px;
height: 200px;
border-radius: 20px;
border: 5px dotted #ff0088;
pointer-events: none;
}
.inputs {
display: flex;
flex-direction: column;
padding-left: 50px;
}
label {
display: flex;
align-items: center;
margin: 10px 0;
}
code {
width: 100px;
font-family: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
font-size: 12px;
}
input {
accent-color: #ff0088;
font-family: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
font-size: inherit;
}
input[type="range"] {
appearance: none;
background: transparent;
}
input[type="number"] {
border: 0;
border-bottom: 1px dotted #ff0088;
color: #ff0088;
margin-left: 10px;
}
input[type="number"]:focus {
outline: none;
border-bottom: 2px solid #ff0088;
}
input[type="number"]::-webkit-inner-spin-button {
-webkit-appearance: none;
}
input[type="range"]::-webkit-slider-runnable-track {
height: 10px;
-webkit-appearance: none;
background: rgb(127 127 127 / 0.12);
border: 1px solid rgb(127 127 127 / 0.25);
border-radius: 10px;
margin-top: -1px;
}
input[type="range"]::-webkit-slider-thumb {
-webkit-appearance: none;
height: 20px;
width: 20px;
border-radius: 50%;
background: #ff0088;
top: -4px;
position: relative;
}
</style> Animatable values
Motion can animate any CSS value, even ones browsers can't animate on their own, like mask-image. It supports:
- Numbers:
0,100and so on. - Strings containing numbers:
"0vh","10px"and so on. - Colors: hex, RGBA and HSLA.
- Complex strings containing several numbers and colors, like
box-shadow. display: "none"/"block"andvisibility: "hidden"/"visible".
Value type conversion
Generally, values animate between two values of the same type, like "0px" to "100px". Colors can animate freely between hex, RGBA and HSLA.
x, y, width, height, top, left, right and bottom can also animate between different value types:
<motion.div initial={{ x: "100%" }} animate={{ x: "calc(100vw - 50%)" }} /> width and height can animate to and from "auto":
<motion.div initial={{ height: 0 }} animate={{ height: "auto" }} /> <script lang="ts">
import { motion } from "motion-sv";
let isOpen = $state(false);
</script>
<div class="accordion">
<button onclick={() => (isOpen = !isOpen)} aria-expanded={isOpen}>
{isOpen ? "Hide details" : "Show details"}
</button>
<motion.div
class="content"
initial={false}
animate={{ height: isOpen ? "auto" : 0, opacity: isOpen ? 1 : 0 }}
transition={{ duration: 0.3 }}
>
<p>
Motion measures the element's natural height, animates to it, then sets <code>height</code> back to
<code>auto</code> so the content can still reflow.
</p>
</motion.div>
</div>
<style>
.accordion {
width: 320px;
border-radius: 12px;
background: rgb(255 62 0 / 0.12);
}
button {
width: 100%;
padding: 0.75rem 1rem;
border: none;
background: none;
color: inherit;
font: inherit;
font-weight: 600;
text-align: left;
cursor: pointer;
}
.accordion :global(.content) {
overflow: hidden;
}
p {
margin: 0;
padding: 0 1rem 1rem;
font-size: 0.875rem;
}
</style> Transforms
Unlike CSS, Motion can animate every transform axis independently:
- Translate:
x,y,z - Scale:
scale,scaleX,scaleY - Rotate:
rotate,rotateX,rotateY,rotateZ - Skew:
skewX,skewY - Perspective:
transformPerspective
motion components have an enhanced style prop that accepts individual transforms too:
<motion.section style={{ x: -20 }} /> Independent transforms are especially flexible with gestures, where each one can animate separately:
<motion.button whileHover={{ scale: 1.1 }} whilePress={{ scale: 0.9 }} /> Independent transforms perform well, but Motion's hybrid engine can also hardware-accelerate an animation when you set transform directly:
<motion.li
initial={{ transform: "translateX(-100px)" }}
animate={{ transform: "translateX(0px)" }}
transition={{ type: "spring" }}
/> Transform origin
transform-origin has three shorthand values that can be set and animated on their own:
originXoriginYoriginZ
As numbers, originX and originY are progress values between 0 and 1. originZ is in pixels.
<motion.div style={{ originX: 0.5 }} /> CSS variables
Motion can animate the value of a CSS variable, and use CSS variables as animation targets.
Animating CSS variables
Animating a CSS variable is a convenient way to animate many children at once:
<motion.ul
initial={{ "--rotate": "0deg" }}
animate={{ "--rotate": "360deg" }}
transition={{ duration: 2, repeat: Infinity }}
>
<li style="transform: rotate(var(--rotate))"></li>
<li style="transform: rotate(var(--rotate))"></li>
<li style="transform: rotate(var(--rotate))"></li>
</motion.ul> CSS variables as animation targets
HTML motion components accept CSS variables as animation targets:
<motion.li animate={{ backgroundColor: "var(--action-bg)" }} /> SVG line drawing
Many SVG elements can draw themselves with three special values: pathLength, pathSpacing and pathOffset.
<motion.path initial={{ pathLength: 0 }} animate={{ pathLength: 1 }} /> All three are progress values between 0 and 1, where 1 is the total length of the path.
<script lang="ts">
import { motion, type Variants } from "motion-sv";
const draw: Variants = {
hidden: { pathLength: 0, opacity: 0 },
visible: (i: number) => {
const delay = i * 0.5;
return {
pathLength: 1,
opacity: 1,
transition: {
pathLength: { delay, type: "spring", duration: 1.5, bounce: 0 },
opacity: { delay, duration: 0.01 },
},
};
},
};
/**
* ============== Styles ================
*/
const image = {
display: "block",
margin: "0 auto",
maxWidth: "80vw",
};
const shape = {
strokeWidth: 10,
strokeLinecap: "round",
fill: "transparent",
};
</script>
<motion.svg width="600" height="600" viewBox="0 0 600 600" initial="hidden" animate="visible" style={image}>
<motion.circle
class="circle-path"
cx="100"
cy="100"
r="80"
stroke="#ff0088"
variants={draw}
custom={1}
style={shape}
/>
<motion.line x1="220" y1="30" x2="360" y2="170" stroke="#4ff0b7" variants={draw} custom={2} style={shape} />
<motion.line x1="220" y1="170" x2="360" y2="30" stroke="#4ff0b7" variants={draw} custom={2.5} style={shape} />
<motion.rect
width="140"
height="140"
x="410"
y="30"
rx="20"
stroke="#0d63f8"
variants={draw}
custom={3}
style={shape}
/>
<motion.circle cx="100" cy="300" r="80" stroke="#0d63f8" variants={draw} custom={2} style={shape} />
<motion.line x1="220" y1="230" x2="360" y2="370" stroke="#ff0088" custom={3} variants={draw} style={shape} />
<motion.line x1="220" y1="370" x2="360" y2="230" stroke="#ff0088" custom={3.5} variants={draw} style={shape} />
<motion.rect
width="140"
height="140"
x="410"
y="230"
rx="20"
stroke="#4ff0b7"
custom={4}
variants={draw}
style={shape}
/>
<motion.circle cx="100" cy="500" r="80" stroke="#4ff0b7" variants={draw} custom={3} style={shape} />
<motion.line x1="220" y1="430" x2="360" y2="570" stroke="#0d63f8" variants={draw} custom={4} style={shape} />
<motion.line x1="220" y1="570" x2="360" y2="430" stroke="#0d63f8" variants={draw} custom={4.5} style={shape} />
<motion.rect
width="140"
height="140"
x="410"
y="430"
rx="20"
stroke="#ff0088"
variants={draw}
custom={5}
style={shape}
/>
</motion.svg> Line drawing works with circle, ellipse, line, path, polygon, polyline and rect elements.
Transitions
By default, Motion picks a transition that suits the value being animated. Physical values like x and scale animate with spring physics, while values like opacity and color animate with a duration and an easing curve.
You can define your own with the transition prop:
<motion.div animate={{ x: 100 }} transition={{ ease: "easeOut", duration: 2 }} /> Enter animations
When a motion component is created, it animates to the values in animate if they differ from the ones it first renders with. Set those starting values with CSS, or with the initial prop:
<motion.li initial={{ opacity: 0, scale: 0 }} animate={{ opacity: 1, scale: 1 }} /> <script lang="ts">
import { motion } from "motion-sv";
</script>
<motion.div
style={{ width: 100, height: 100, backgroundColor: "#8df0cc", borderRadius: "50%" }}
initial={{ opacity: 0, scale: 0 }}
animate={{ opacity: 1, scale: 1 }}
transition={{
duration: 0.4,
scale: { type: "spring", visualDuration: 0.4, bounce: 0.5 },
}}
/> Set initial to false to disable the enter animation. The element renders with the values in animate straight away.
<motion.div initial={false} animate={{ y: 100 }} /> Exit animations
Svelte removes elements from the DOM as soon as they leave an {#if} or {#each} block. Wrap them in AnimatePresence and they stay in the DOM while their exit animation plays:
<script lang="ts">
import { AnimatePresence, motion } from "motion-sv";
let isVisible = $state(true);
</script>
<AnimatePresence>
{#if isVisible}
<motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }} />
{/if}
</AnimatePresence> <script lang="ts">
import { AnimatePresence, motion } from "motion-sv";
let isVisible = $state(true);
</script>
<div class="container">
<AnimatePresence initial={false}>
{#if isVisible}
<motion.div
class="box"
initial={{ opacity: 0, scale: 0 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0 }}
/>
{/if}
</AnimatePresence>
<motion.button class="button" onclick={() => (isVisible = !isVisible)} whilePress={{ y: 1 }}>
{isVisible ? "Hide" : "Show"}
</motion.button>
</div>
<style>
.container {
display: flex;
flex-direction: column;
width: 100px;
height: 160px;
position: relative;
}
.container :global(.box) {
width: 100px;
height: 100px;
background-color: #0cdcf7;
border-radius: 10px;
}
.container :global(.button) {
background-color: #0cdcf7;
border: none;
border-radius: 10px;
padding: 10px 20px;
color: #0b1012;
font: inherit;
font-size: 15px;
font-weight: 540;
line-height: 1;
cursor: pointer;
position: absolute;
bottom: 0;
left: 0;
right: 0;
}
</style> Keyframes
Values in animate can be arrays of keyframes. The value animates through each one in turn:
<motion.div animate={{ x: [0, 100, 0] }} /> <script lang="ts">
import { motion } from "motion-sv";
</script>
<motion.div
style={{ width: 100, height: 100, backgroundColor: "currentColor", borderRadius: 5 }}
animate={{
scale: [1, 2, 2, 1, 1],
rotate: [0, 0, 180, 180, 0],
borderRadius: ["0%", "0%", "50%", "50%", "0%"],
}}
transition={{
duration: 2,
ease: "easeInOut",
times: [0, 0.2, 0.5, 0.8, 1],
repeat: Infinity,
repeatDelay: 1,
}}
/> Use null as the first keyframe to start from the value's current state:
<motion.div animate={{ x: [null, 100, 0] }} /> <script lang="ts">
import { motion } from "motion-sv";
</script>
<motion.div
style={{ width: 100, height: 100, backgroundColor: "#0cdcf7", borderRadius: 5 }}
whileHover={{
scale: [null, 1.1, 1.6],
transition: {
duration: 0.5,
times: [0, 0.6, 1],
ease: ["easeInOut", "easeOut"],
},
}}
transition={{
duration: 0.3,
ease: "easeOut",
}}
/> If this animation interrupts another, it continues from wherever the value is, so the change feels natural. Hover the box above, move away and hover again before it settles: the scale picks up from where it is rather than jumping back to 1.
By default, keyframes are spaced evenly through the animation. To place them yourself, set the times option of the transition. times is an array of progress values between 0 and 1, one per keyframe:
<motion.circle
cx={500}
animate={{
cx: [null, 100, 200],
transition: { duration: 3, times: [0, 0.2, 1] },
}}
/> Gesture animations
motion components have props that animate to a target while a gesture is active, and back again when it ends:
<motion.button
initial={{ opacity: 0 }}
whileHover={{ backgroundColor: "rgba(220, 220, 220, 1)" }}
whilePress={{ backgroundColor: "rgba(255, 255, 255, 1)" }}
whileInView={{ opacity: 1 }}
/> <script lang="ts">
import { motion } from "motion-sv";
</script>
<div class="example">
<motion.div whileHover={{ scale: 1.2 }} whilePress={{ scale: 0.8 }} class="box" />
</div>
<style>
.example :global(.box) {
width: 100px;
height: 100px;
border-radius: 10px;
background-color: #1e75f7;
}
</style> There are props for hover, press, focus, drag and in view: whileHover, whilePress, whileFocus, whileDrag and whileInView. Learn more about gestures.
Variants
Setting animate to a target works well for animating a single element. To orchestrate animations through the DOM, use variants.
Variants are a set of named targets:
const variants = {
visible: { opacity: 1 },
hidden: { opacity: 0 },
}; Pass them to a motion component with the variants prop:
<motion.div {variants} /> You can now refer to them by name anywhere you'd set an animation target:
<motion.div {variants} initial="hidden" animate="visible" /> To apply several variants at once, pass an array of names:
<motion.div animate={["visible", "danger"]} /> Propagation
Variants are useful for reusing and combining targets, but their real power is orchestrating animations through a tree.
Variants flow down through motion components. Here, when the ul enters the viewport, every child with a visible variant animates in too:
<script lang="ts">
import { motion, type Variants } from "motion-sv";
const list: Variants = {
visible: { opacity: 1 },
hidden: { opacity: 0 },
};
const item: Variants = {
visible: { opacity: 1, x: 0 },
hidden: { opacity: 0, x: -100 },
};
</script>
<motion.ul initial="hidden" whileInView="visible" variants={list}>
<motion.li variants={item} />
<motion.li variants={item} />
<motion.li variants={item} />
</motion.ul> The children don't set initial, animate or whileInView themselves: they inherit the parent's variant names, and animate with their own definition of each.
In this menu, only the nav sets animate. The background, the list, each item and the icon's lines all follow it between "open" and "closed", each with its own variants:
<script lang="ts">
import { motion, stagger, type Variants } from "motion-sv";
let isOpen = $state(false);
let nav = $state<HTMLElement | SVGElement | null>(null);
let height = $state(0);
$effect(() => {
if (nav instanceof HTMLElement) height = nav.offsetHeight;
});
const toggle = () => {
isOpen = !isOpen;
};
const navVariants: Variants = {
open: {
transition: { delayChildren: stagger(0.07, { startDelay: 0.2 }) },
},
closed: {
transition: { delayChildren: stagger(0.05, { from: "last" }) },
},
};
const itemVariants: Variants = {
open: {
y: 0,
opacity: 1,
transition: {
y: { stiffness: 1000, velocity: -100 },
},
},
closed: {
y: 50,
opacity: 0,
transition: {
y: { stiffness: 1000 },
},
},
};
const sidebarVariants: Variants = {
open: (height = 1000) => ({
clipPath: `circle(${height * 2 + 200}px at 40px 40px)`,
transition: {
type: "spring",
stiffness: 20,
restDelta: 2,
},
}),
closed: {
clipPath: "circle(30px at 40px 40px)",
transition: {
delay: 0.2,
type: "spring",
stiffness: 400,
damping: 40,
},
},
};
const colors = ["#FF008C", "#D309E1", "#9C1AFF", "#7700FF", "#4400FF"];
</script>
<div class="container">
<motion.nav
initial={false}
animate={isOpen ? "open" : "closed"}
custom={height}
bind:ref={nav}
class="nav"
>
<motion.div class="background" variants={sidebarVariants} />
<!-- Navigation -->
<motion.ul class="list" variants={navVariants}>
{#each colors as color (color)}
<motion.li
class="list-item"
variants={itemVariants}
whilePress={{ scale: 0.95 }}
whileHover={{ scale: 1.1 }}
>
<div class="icon-placeholder" style:border="2px solid {color}"></div>
<div class="text-placeholder" style:border="2px solid {color}"></div>
</motion.li>
{/each}
</motion.ul>
<!-- Menu Toggle -->
<button class="toggle-container" onclick={toggle} aria-label="Toggle menu">
<svg width="23" height="23" viewBox="0 0 23 23">
<motion.path
fill="transparent"
stroke-width="3"
stroke="hsl(0, 0%, 18%)"
stroke-linecap="round"
variants={{
closed: { d: "M 2 2.5 L 20 2.5" },
open: { d: "M 3 16.5 L 17 2.5" },
}}
/>
<motion.path
fill="transparent"
stroke-width="3"
stroke="hsl(0, 0%, 18%)"
stroke-linecap="round"
d="M 2 9.423 L 20 9.423"
variants={{
closed: { opacity: 1 },
open: { opacity: 0 },
}}
transition={{ duration: 0.1 }}
/>
<motion.path
fill="transparent"
stroke-width="3"
stroke="hsl(0, 0%, 18%)"
stroke-linecap="round"
variants={{
closed: { d: "M 2 16.346 L 20 16.346" },
open: { d: "M 3 2.5 L 17 16.346" },
}}
/>
</svg>
</button>
</motion.nav>
</div>
<style>
.container {
position: relative;
display: flex;
justify-content: flex-start;
align-items: stretch;
width: 500px;
max-width: 100%;
height: 400px;
background-color: #0cdcf7;
border-radius: 20px;
overflow: hidden;
}
.container :global(.nav) {
width: 300px;
}
.container :global(.background) {
background-color: #f5f5f5;
position: absolute;
top: 0;
left: 0;
bottom: 0;
width: 300px;
}
.toggle-container {
outline: none;
border: none;
user-select: none;
cursor: pointer;
position: absolute;
top: 18px;
left: 15px;
width: 50px;
height: 50px;
border-radius: 50%;
background: transparent;
}
.container :global(.list) {
list-style: none;
padding: 25px;
margin: 0;
position: absolute;
top: 80px;
width: 230px;
}
.container :global(.list-item) {
display: flex;
align-items: center;
justify-content: flex-start;
padding: 0;
margin: 0;
list-style: none;
margin-bottom: 20px;
cursor: pointer;
}
.icon-placeholder {
width: 40px;
height: 40px;
border-radius: 50%;
flex: 40px 0;
margin-right: 20px;
}
.text-placeholder {
border-radius: 5px;
width: 200px;
height: 20px;
flex: 1;
}
</style> Orchestration
By default, children animate at the same time as their parent. Variants unlock extra transition options that change this, like when and delayChildren:
import { stagger, type Variants } from "motion-sv";
const list: Variants = {
visible: {
opacity: 1,
transition: {
when: "beforeChildren",
delayChildren: stagger(0.3), // Stagger children by 0.3 seconds
},
},
hidden: {
opacity: 0,
transition: {
when: "afterChildren",
},
},
}; when: "beforeChildren" finishes the parent's animation before the children start, and "afterChildren" waits for the children to finish first. stagger spreads the children's delays, from the first child to the last by default.
Dynamic variants
A variant can be a function, which runs when the variant becomes active:
const variants: Variants = {
hidden: { opacity: 0 },
visible: (index: number) => ({
opacity: 1,
transition: { delay: index * 0.3 },
}),
}; The function receives the component's custom prop, so each element can resolve the same variant differently:
{#each items as item, index (item.id)}
<motion.div custom={index} {variants} initial="hidden" animate="visible" />
{/each} Animation controls
Declarative animations are ideal for most UI, but sometimes you need to start animations yourself. useAnimationControls() creates controls that you pass to animate instead of a target:
<script lang="ts">
import { motion, useAnimationControls } from "motion-sv";
const controls = useAnimationControls();
async function play() {
await controls.start({ x: 160 });
await controls.start({ rotate: 90 });
await controls.start({ x: 0, rotate: 0 });
}
</script>
<motion.div animate={controls} />
<button onclick={play}>Play</button> Call useAnimationControls() at the top level of the <script>, and only call its methods once the component has mounted, in an event handler or an $effect.
start(definition, transition?)animates every component that uses these controls.definitioncan be a target, a variant name, an array of variant names, or a function, which each component resolves with its owncustomprop. It returns a promise that resolves when all the animations have finished.set(definition)sets the values instantly.stop()stops the animations.
<script lang="ts">
const controls = useAnimationControls();
function play() {
controls.start((i: number) => ({ x: 160, transition: { delay: i * 0.1 } }));
}
</script>
{#each [0, 1, 2] as i (i)}
<motion.div custom={i} animate={controls} />
{/each} <script lang="ts">
import { motion, useAnimationControls } from "motion-sv";
const controls = useAnimationControls();
async function play() {
await controls.start((i: number) => ({ x: 160, transition: { delay: i * 0.1 } }));
await controls.start({ rotate: 90, borderRadius: "50%" });
await controls.start({ x: 0, rotate: 0, borderRadius: "20%" });
}
</script>
<div class="controls-demo">
<div class="track">
{#each [0, 1, 2] as i (i)}
<motion.div class="box" custom={i} animate={controls} />
{/each}
</div>
<div class="buttons">
<button onclick={play}>Play</button>
<button onclick={() => controls.stop()}>Stop</button>
</div>
</div>
<style>
.controls-demo {
display: flex;
flex-direction: column;
gap: 1.5rem;
width: 260px;
}
.track {
display: flex;
flex-direction: column;
gap: 12px;
}
.track :global(.box) {
width: 50px;
height: 50px;
border-radius: 20%;
background: #0cdcf7;
}
.buttons {
display: flex;
gap: 0.5rem;
}
button {
padding: 8px 16px;
border: none;
border-radius: 8px;
background: rgb(12 220 247 / 0.2);
color: inherit;
font: inherit;
cursor: pointer;
}
</style> For full imperative control, use useAnimate. It can:
- Animate any HTML or SVG element, not just
motioncomponents. - Run complex animation sequences.
- Control playback with
time,speed,play(),pause()and the other playback controls.
<script lang="ts">
import { useAnimate } from "motion-sv";
const [scope, animate] = useAnimate();
$effect(() => {
const controls = animate([
[scope.current, { x: "100%" }],
["li", { opacity: 1 }],
]);
controls.speed = 0.8;
return () => controls.stop();
});
</script>
<ul bind:this={scope.current}>
<li></li>
<li></li>
<li></li>
</ul> Animate content
Motion values aren't Svelte state, so changing one doesn't update the markup. To render a motion value's latest value, read it through useCurrentMotionValue, whose current property is reactive:
<script lang="ts">
import { animate, useCurrentMotionValue, useMotionValue, useTransform } from "motion-sv";
const count = useMotionValue(0);
const rounded = useTransform(() => Math.round(count.get()));
const latest = useCurrentMotionValue(rounded);
$effect(() => {
const controls = animate(count, 100, { duration: 5 });
return () => controls.stop();
});
</script>
<pre>{latest.current}</pre> <script lang="ts">
import { animate, useCurrentMotionValue, useMotionValue, useTransform } from "motion-sv";
const count = useMotionValue(0);
const rounded = useTransform(() => Math.round(count.get()));
const latest = useCurrentMotionValue(rounded);
$effect(() => {
const controls = animate(count, 100, { duration: 5 });
return () => controls.stop();
});
</script>
<div class="motion-count">{latest.current}</div>
<style>
.motion-count {
font-size: 64px;
color: #4ff0b7;
display: flex;
justify-content: center;
align-items: center;
}
</style> As the value changes, Svelte updates only the text that reads latest.current. Because rounded only changes when the whole number does, the text updates at most 100 times, not on every frame.