Why smooth animations matter in production
Jarring UI transitions break user trust faster than you'd expect. When a modal slides in, a counter ticks up, or a chart animates into view, users notice whether motion feels natural or robotic. In this instalment of the 100-day Svelte & SvelteKit series, we're covering motion with tweened and spring: best practices explained—the core tools that let you orchestrate those smooth animated values without reaching for external animation libraries.
We've built enough dashboards, analytics platforms, and real-time trading interfaces to know that a 200-millisecond tween can mean the difference between "feels cheap" and "feels premium." Let's dig into how Svelte's reactive motion primitives work, when to use each one, and how to avoid the pitfalls that catch teams mid-project.
Tween and Spring classes in Svelte 5
Svelte 5 ships with two motion stores: tweened and spring. Both create reactive stores that animate from one value to another, but they behave very differently.
Tweened stores follow a linear or easing-based path over a fixed duration. You say "I want to go from 0 to 100 in 500 milliseconds," and it does exactly that, following an easing curve like cubicInOut or quadIn. Great for controlled, predictable animations.
Spring stores simulate physics. You set a target value and the store oscillates toward it, overshooting slightly before settling. No duration parameter—instead, you tune stiffness and damping. This feels natural for interactive elements: dragging a slider, hovering a button, or snapping to a new state.
Both are imported from svelte/motion:
import { tweened, spring } from 'svelte/motion';
const progress = tweened(0, {
duration: 1000,
easing: cubicInOut
});
const position = spring(0, {
stiffness: 0.1,
damping: 0.3
});Under the hood, tweened uses requestAnimationFrame to tick through values, while spring recalculates velocity and position each frame to simulate physical motion. Both are memory-efficient and play nicely with Svelte's reactivity—subscribe once, update whenever the store changes.
Animate numbers and objects
The simplest case: animating a single number. Think progress bars, counters, or percentage displays in a SaaS dashboard.
import { tweened } from 'svelte/motion';
import { cubicInOut } from 'svelte/easing';
let progress = tweened(0, {
duration: 800,
easing: cubicInOut
});
function startUpload() {
progress.set(100);
}In your component, bind to $progress and watch it smoothly climb. No manual RAF loops, no state machine. Svelte handles the lifecycle.
But real-world scenarios are messier. Charts need animated coordinates, modals need animated offsets and opacity, and multi-value states need synchronized animations. This is where object animation shines.
Pass an object to tweened and it will interpolate each key independently:
import { tweened } from 'svelte/motion';
import { cubicInOut } from 'svelte/easing';
let chartPoint = tweened(
{ x: 10, y: 50, opacity: 0 },
{ duration: 600, easing: cubicInOut }
);
function updateChart() {
chartPoint.set({ x: 200, y: 150, opacity: 1 });
}Svelte will interpolate x, y, and opacity as independent number tweens. When you subscribe or bind to $chartPoint, you get the current object with all three values in flight.
For arrays of objects—a list of bars in a chart, rows in a table with animated background colours—apply tweens per item and let Svelte's keying handle cleanup:
let items = [
{ id: 1, value: 10 },
{ id: 2, value: 30 }
];
let animatedItems = items.map(item => ({
...item,
animated: tweened(item.value, { duration: 400 })
}));
{#each animatedItems as item (item.id)}
<div>
<div class="bar" style="width: {$item.animated}px"></div>
</div>
{/each}The key here is immutability and per-item store creation. Create a tween per data item, update its target with .set(), and let the framework handle subscriptions. Avoid recreating tweens on every state change—that kills the animation.
Great for charts and progress
Animating data is where tweened and spring shine. We've used this pattern in several analytics dashboards: when a user filters by date range or applies a segment, numbers should fade in smoothly, not snap.
Here's a realistic example: a KPI card that animates when the value updates:
import { tweened } from 'svelte/motion';
import { cubicInOut } from 'svelte/easing';
let displayValue = tweened(0, {
duration: 800,
easing: cubicInOut
});
$: displayValue.set(kpiValue);The reactive statement watches kpiValue and triggers a new animation any time it changes. Clean. No manual animation loops.
For progress bars with indeterminate states (uploads, background jobs), combine tweened with conditional logic:
let uploadProgress = tweened(0, {
duration: 500,
easing: cubicInOut
});
let isUploading = false;
async function upload(file) {
isUploading = true;
uploadProgress.set(50);
// Simulate upload time
await new Promise(r => setTimeout(r, 1000));
uploadProgress.set(100);
isUploading = false;
}Charts are a natural fit. Bar charts, line charts, gauge visualizations—all benefit from smooth transitions when data changes. Instead of redrawing instantly, tween the numeric values and redraw at each animation frame. SVG data-bind patterns make this trivial.
A practical tip: in high-frequency data scenarios (real-time trading, live dashboards), debounce updates or skip tweening on every single tick. If data arrives 10 times per second, animating every update creates visual noise. Debounce to once per 200 milliseconds and let the tween catch up.
Tune duration and stiffness
Getting the feel right requires understanding the parameters.
For tweened: duration is in milliseconds. A 300–500ms range feels natural for most UI transitions. Faster (100–200ms) for micro-interactions. Slower (800–1200ms) for data-heavy visualizations or data loads. Always consider your user's device—a mobile phone with a slower GPU may feel sluggish at 60fps if your easing curve is complex.
// Quick feedback: button press, toggle
let fast = tweened(0, { duration: 150 });
// Standard UI motion: modal, panel
let standard = tweened(0, { duration: 400 });
// Data visualization: chart update, large number
let slow = tweened(0, { duration: 1000 });Easing functions matter. cubicInOut is the safe default—accelerates at the start, decelerates at the end. quadIn for sharp starts. elasticOut for playful bounces. Import from svelte/easing and experiment locally before shipping.
For spring: stiffness controls how fast the value accelerates toward its target. Damping controls how much it overshoots and bounces. The interplay is physics-based, not dial-and-forget.
// Stiff, quick snap with slight bounce
let snappy = spring(0, { stiffness: 0.2, damping: 0.4 });
// Smooth, floaty, slow settle
let floaty = spring(0, { stiffness: 0.05, damping: 0.8 });
// Very bouncy, deliberate overshoot
let bouncy = spring(0, { stiffness: 0.15, damping: 0.1 });In production, we typically see stiffness between 0.05 and 0.3, damping between 0.2 and 0.9. Start conservative (0.1 stiffness, 0.5 damping) and adjust based on user feedback. High stiffness + low damping = bouncy. Low stiffness + high damping = sluggish.
Spring is ideal for interactive motion: dragging a handle, responding to pointer events, or building custom sliders. Tweened is better for one-shot animations or data transitions. Don't overthink it—if you're unsure, start with tweened and a 400ms duration. You can always refine.
Common mistakes
Recreating tweens on every render. If you define let value = tweened(0) inside a reactive block or component initialization without proper scoping, Svelte will reset the animation. Define tweens at the component level, not inside reactive statements.
Ignoring the store subscription. Remember that tweened and spring return stores. You must subscribe (with $ in templates or .subscribe() in code) to get values. Forgetting this leaves you with a store object, not the animated value.
Mismatching easing curves to intent. Linear easing looks robotic for UI. Sine or cubic curves feel more natural. Test with actual users, not in isolation.
Chaining animations without coordination. If you have multiple tweens that should start together, they will—unless you accidentally call .set() at different times. Use reactive statements to keep them in sync.
Over-animating in lists. If you have 100 rows in a table and each one is a separate tween, browser performance degrades. Use CSS animations for large batches and reserve Svelte tweens for critical paths.
Forgetting to handle initial values. If your tween starts at 0 but data is already loaded, the initial animation fires before the user sees anything. Pre-set the store to the current value before attaching it to data.
Key takeaways
- Tweened is for predictable, duration-based animations with easing curves. Spring is for physics-based, interactive motion.
- Both animate numbers, objects, and arrays—just keep tweens scoped at the component level and update with
.set(). - Charts, progress bars, and KPI cards benefit enormously from smooth transitions. Users perceive them as more polished and responsive.
- Tune duration for tweened (300–500ms is standard). For spring, experiment with stiffness (0.05–0.3) and damping (0.2–0.9).
- Subscribe to the store value in your template with
$storeNameor in code with.subscribe(). - Avoid recreating tweens on every state change. Define them once and update their targets reactively.
- In high-frequency data scenarios, debounce updates to avoid animation spam.
- Test easing curves and durations with real users on real devices before assuming they feel right.






Comments
Loading comments…