Motion
Motion in Settld is a physical feedback system, not decoration. Every animation answers the question: why does this animate? Press states feel like real buttons. Sheet entrances feel like paper sliding in. Transitions confirm that the interface heard you. If an animation can't answer that question, it doesn't ship.
Philosophy
Motion is not about beauty — it's about communication. A button that scales down on press tells you it registered your tap. A sheet that eases in from below tells you it came from somewhere. These are not aesthetic choices; they are functional signals that happen to look good when done right.
Ask this before writing any animation: how often will a user see it? A button press happens dozens of times a day — it must be 140ms and instant. A sheet entrance happens a handful of times — 420ms with an iOS curve is earned. An onboarding animation happens once — it can be slow and expressive. The frequency determines the budget.
Never animate from scale(0). Nothing in the real world appears from nothing. Popovers start at scale(0.95) with opacity 0. Sheets slide up from translateY(100%). Cards that enter a list fade up from translateY(8px). The starting state should be a compressed or offset version of the final state — not a void.
All those unseen details — the press state that barely registers, the stagger that feels like items are arriving rather than appearing, the easing curve that starts fast and settles — combine into something users describe as "polished" without ever naming what they noticed. That's the goal.
Easing curves
Three curves. Each has a job. Out is the default — it starts fast and settles, which makes the interface feel immediately responsive. Spring overshoots slightly — for toggles and pops where a physical bounce signals a state change. iOS is the drawer/sheet curve — slower start, confident finish — it's how Apple's sheets feel.
Click the track to play each curve.
--ease-out
--ease-spring
--ease-ios
Duration tokens
Three durations. Fast for interactions, medium for transitions, slow for structural changes. Everything stays under 420ms — anything longer makes the UI feel like it's loading, not animating. Click each track to feel the difference.
Press states
Every tappable element — buttons, cards, list items, chips — gets scale(0.97) on :active. This is not optional. Without a press state, the interface feels like it didn't hear you. With it, the tap has physical weight. 3% is enough to register; more than that looks broken.
Press the cards below.
/* Press state — every interactive element */ .card { transition: transform var(--d-fast) var(--ease-out), box-shadow var(--d-fast) var(--ease-out); } .card:active { transform: scale(0.97); box-shadow: var(--shadow-sm); } /* Never use transition: all — enumerate exactly */ /* Bad: transition: all 140ms ease-out; */ /* Good: transition: transform 140ms var(--ease-out), background 140ms var(--ease-out); */
Hover gating
Hover animations are gated behind @media (hover: hover) and (pointer: fine). On touch devices, :hover fires on tap and stays sticky — so hover fills and transforms would get stuck on. The media query ensures hover effects only run where a real cursor exists.
/* Gate all hover transforms and fills */ @media (hover: hover) and (pointer: fine) { .card:hover { background: var(--s-warm); box-shadow: var(--shadow-md); } .btn-primary:hover { background: var(--teal-deep); } } /* Press states (:active) do NOT need this gate — they fire correctly on touch. Only hover does. */
Scroll reveal & stagger
When multiple elements enter together — a list loading, cards appearing after a filter — stagger their appearance 50–80ms apart. Everything appearing at once looks like a hard cut. Staggered entries look like items arriving, which feels natural and gives the eye a path to follow.
Keep stagger delays short. Long stagger makes the interface feel sluggish. The animation is decorative — never block interaction while stagger plays.
/* Stagger: 50-80ms between siblings */ .list-item { opacity: 0; transform: translateY(8px); animation: fade-up 280ms var(--ease-out) forwards; } .list-item:nth-child(1) { animation-delay: 0ms; } .list-item:nth-child(2) { animation-delay: 60ms; } .list-item:nth-child(3) { animation-delay: 120ms; } .list-item:nth-child(4) { animation-delay: 180ms; } @keyframes fade-up { to { opacity: 1; transform: translateY(0); } }
Entry animation rules
Elements that appear must enter from a plausible physical state. Popovers and tooltips scale up from near their final size. Toasts slide in from the direction they'll be dismissed to. Sheets enter from below — that's where the thumb is. Never reveal from scale(0).
| Element | Enter from | Duration | Easing |
|---|---|---|---|
| Button press | scale(1) → scale(0.97) → scale(1) | fast (140ms) | --ease-out |
| Popover / menu | scale(0.95) + opacity 0 | med (240ms) | --ease-out |
| Toast | translateY(100%) + opacity 0 | med (240ms) | --ease-out |
| Modal | scale(0.96) + opacity 0, centered | med (240ms) | --ease-out |
| Sheet / drawer | translateY(100%) | slow (420ms) | --ease-ios |
| List items (scroll reveal) | translateY(8px) + opacity 0 | 280ms staggered | --ease-out |
| Toggle / chip (state change) | scale(0.95) + spring overshoot | med (240ms) | --ease-spring |
Token reference
| Token | Value | Purpose |
|---|---|---|
--ease-out | cubic-bezier(0.23, 1, 0.32, 1) | Default for all UI transitions |
--ease-spring | cubic-bezier(0.34, 1.56, 0.64, 1) | Toggles, state pops, celebration moments |
--ease-ios | cubic-bezier(0.32, 0.72, 0, 1) | Sheets and drawers |
--d-fast | 140ms | Hover, press, small reveals |
--d-med | 240ms | Popovers, cards, toasts |
--d-slow | 420ms | Sheets, mode switches, bar resizes |
Usage
scale(0.97) on :active for every pressable element. Gate hover animations behind the pointer media query. Start entries from a near-final state, never from void.
transition: all — it animates layout properties (padding, border) and causes jank. Don't animate from scale(0). Don't autoplay ambient animations that users see on every visit — they become noise.
prefers-reduced-motion. Keep opacity and color transitions (they aid comprehension). Remove position and scale animations. Reduced motion means less motion, not no feedback.
ease-in for UI animations. It starts slow and makes the interface feel laggy before it moves. A 240ms ease-in dropdown feels slower than 240ms ease-out because the delay hits right when the user is watching.
Accessibility
@media (prefers-reduced-motion: no-preference) guard. Leave color and opacity transitions intact — they carry meaning. The press state scale(0.97) is so small it's safe to keep even in reduced motion.
@media (prefers-reduced-motion: no-preference) { .sheet { transition: transform var(--d-slow) var(--ease-ios); } .list-item { animation: fade-up 280ms var(--ease-out) forwards; } } /* Safe in all modes — opacity carries meaning */ .toast { transition: opacity var(--d-med) var(--ease-out); } /* Safe in all modes — press state is barely perceptible */ .btn:active { transform: scale(0.97); }