Motion & sound
Colour and type say what a thing is. Motion and sound say what it is doing: a cap going down, a panel arriving, a readout ticking over. In Echo both are tokens — named, shared, and swappable — so the whole device moves and sounds like one object.
Hear them in the Sound audition; feel them together on the Tactile bench.
What each token means
The tables further down are the physics — a waveform, a duration, a curve. This is the part you have to decide: which token belongs to what you are building. Pick by meaning, never by how the name sounds.
| Token | What it means | Used by |
|---|---|---|
press | A part goes down under your finger, and comes back with a little overshoot. 90ms — an acknowledgement, not an animation. | 2: Motion tokens, useTactile |
pop | Something small arrives: a chip, a badge, a count. It overshoots, the way a light part would. | 16: Motion tokens, useTactile, Collapsible, Toggle… |
summon | Something larger arrives or leaves: a panel, a sheet, a dialog. The only family that fades under reduced motion instead of stopping. | 6: Motion tokens, useGsapPresence, Modal, Popover… |
breath | A slow loop that says a thing is alive and idle — a status light, a meter at rest. It never demands attention. | 1: Motion tokens |
settle | A part finds its final position after being moved, dragged or released. Slower, and it decelerates. | 1: Motion tokens |
glitch | A screen changes. `steps(3)`, never an ease: this is a segment display, and glass timing on it looks wrong. | 2: Motion tokens, Command |
flood | A surface is taken over — a wash of light, a value sweeping across a track. | 3: Motion tokens, Receipt, Ticket |
drain | Time passing, scrubbed by a clock rather than played. Linear, because nothing about elapsed time accelerates. | 1: Motion tokens |
lift | A part notices the pointer and rises toward the light before anything is pressed. Quick, with a small overshoot. | 5: Motion tokens, HardwareButton, Switch, NumberField… |
follow | A part trails the hand: a knob being dragged, a cap leaning toward the cursor. Short, so it feels heavy but never late. | 2: Motion tokens, Modal |
travel | A part lands somewhere new — a knob sent across a rail, a switch thrown, a selection moving over. It springs into place. | 3: Motion tokens, Table, WidgetGrid |
squash | A part gives under the finger as it is grabbed. Faster than a press, and it holds until released. | 1: Motion tokens |
rebound | A part is let go and springs back past rest before it settles. The overshoot is what says it has weight. | 2: Motion tokens, Switch |
light | A highlight tracks the pointer across a glossy surface. Smooth, because light has no mass. | 3: Motion tokens, HardwareButton, LedKey |
bloom | A popup grows out of what opened it — a menu from its trigger, a tooltip from its target — overshooting a touch and settling. Solid almost at once, so it never reads as a fade. | 2: Motion tokens, useGsapPresence |
island | A panel swells out of a small pill, like an island opening: wider first, then taller, on a spring that settles with a soft wobble. Closing gathers it back into the pill. | 2: Motion tokens, Modal |
boot | A readout powers on: cells light in steps, digits count up. Stepped like glitch, because it is still a screen. | 1: Motion tokens |
| Token | What it means | Used by |
|---|---|---|
tap | The lightest mark: a key in a field, a step in a sequence. Quieter and higher than a press. | 13: Sound Engine, Card, Item, Collapsible… |
press | A cap has gone down. It bends downward in pitch, the way a real key loads. | 5: Sound Engine, useTactile, HardwareButton, LedKey… |
release | The cap has come back up and the action has happened. | 5: Sound Engine, useTactile, HardwareButton, LedKey… |
select | One of several has been chosen — a tab, an option, an item in a menu. | 13: Sound Engine, Combobox, CheckboxGroup, ChoiceCards… |
transition | Something has arrived or left: a panel, a sheet, a drawer. It bends upward. | 5: Sound Engine, Command, Modal, Sheet… |
notify | Something happened that you did not ask for, now. Two tones, so it is not mistaken for a press. | 2: Sound Engine, PullToRefresh |
caution | The one rough sound. A confirm that deserves a second of hesitation, and nothing else. | 2: Sound Engine, AlertDialog |
error | An action was refused, or a destructive one went through. Low and falling. | 2: Sound Engine, AlertDialog |
celebrate | A finished thing worth marking. Spend it rarely; it stops meaning anything otherwise. | 1: Sound Engine |
A token nothing uses yet is not a gap; it is there for your own controls. But if you find yourself wanting a meaning that is not on this list, add a token rather than an inline duration — that is how a motion system stays one system.
Motion
Motion tokens are GSAP timings. Components name a token — press, summon — instead of writing a
duration and a curve.
| Token | Duration | Ease | Reduced motion |
|---|---|---|---|
press | 90ms | back.out(2) | none — no movement |
pop | 400ms | back.out(2.5) | none — no movement |
summon | 300ms | power2.out | opacity only |
breath | 4000ms | sine.inOut · loops | none — no movement |
settle | 600ms | power1.inOut | none — no movement |
glitch | 80ms | steps(3) | none — no movement |
flood | 700ms | power3.inOut | opacity only |
drain | scrubbed | linear | none — no movement |
lift | 280ms | back.out(3) | none — no movement |
follow | 180ms | power3.out | none — no movement |
travel | 560ms | back.out(1.6) | none — no movement |
squash | 110ms | power2.out | none — no movement |
rebound | 700ms | elastic.out(1.1, 0.45) | none — no movement |
light | 450ms | power2.out | none — no movement |
boot | 520ms | steps(6) | opacity only |
bloom | 460ms | back.out(1.5) | opacity only |
island | 600ms | elastic.out(1, 0.75) | opacity only |
The rule behind them: glass moves smooth, pixels move steppy. Panels and caps ease; anything on
an LCD screen changes in steps (glitch), the way a real segment display does. A readout that eases
smoothly looks wrong on this material.
CSS transitions use a matching scale, emitted by the theme:
| Step | Duration | Ease |
|---|---|---|
--echo-duration-glitch | 80ms | steps(3, end) |
--echo-duration-press | 90ms | cubic-bezier(0.34, 1.56, 0.64, 1) |
--echo-duration-tick | 120ms | ease |
--echo-duration-swap | 160ms | ease-out |
--echo-duration-glide | 190ms | cubic-bezier(0.2, 0.8, 0.2, 1) |
--echo-duration-sweep | 220ms | cubic-bezier(0.4, 0, 0.2, 1) |
--echo-duration-pop | 400ms | cubic-bezier(0.34, 1.56, 0.64, 1) |
--echo-duration-flood | 700ms | cubic-bezier(0.65, 0, 0.35, 1) |
Reduced motion
When the OS asks for reduced motion, every token follows its own rule, shown above: most stop moving
entirely, and the ones that bring something on screen (summon, flood) fade instead. Nothing is
left moving that the reader asked to stop.
Sound
A sound token is a recipe, not a file: a waveform, a pitch, a length and a volume, played by a tiny WebAudio synthesiser. There are no audio assets to load or license, and a recipe is data you can edit.
| Token | Wave | Pitch | Length | Gain | Haptic |
|---|---|---|---|---|---|
tap | square | 2200 Hz | 30ms | 0.08 | none |
press | square | 1800 Hz (bend -400 Hz) | 40ms | 0.12 | none |
release | square | 1400 Hz | 30ms | 0.08 | none |
select | triangle | 1100 Hz | 50ms | 0.1 | tick |
transition | sine | 700 Hz (bend +250 Hz) | 80ms | 0.08 | none |
notify | sine | 660 → 990 Hz | 150ms | 0.15 | success |
caution | sawtooth | 330 Hz | 180ms | 0.14 | warn |
error | sawtooth | 220 Hz (bend -60 Hz) | 200ms | 0.15 | error |
celebrate | square | 523 → 659 → 784 Hz | 250ms | 0.14 | success |
The aim is dry, short and hardware: clicks and chirps from a device, not notes from an instrument. Every play varies the pitch by up to 3% and the volume by up to 10%, so repeated presses do not sound mechanical.
SoundProvider
Components play sound through a provider, and are silent without one:
import { SoundProvider } from "echo-ui-core/sound";
<SoundProvider masterGain={0.5} muteOnReducedMotion>
{children}
</SoundProvider>It takes care of the parts that are easy to get wrong:
- No audio before a gesture. The audio context starts on the first pointer press, as browsers require, and closes when the provider unmounts, so one inside a page leaves nothing running.
- It starts muted. Someone who hasn't chosen yet hears nothing until they turn sound on, so a
first visit makes no sound nobody asked for. Give them a switch (
setMutedbelow), or passdefaultMuted={false}where sound is the point from the first press. - Mute is remembered, in
localStorage(echo-sound-muted), and a stored choice always wins. - Silent when you are not looking — nothing plays in a hidden or unfocused tab.
- Reduced motion mutes too, by default (
muteOnReducedMotion). - Master volume at half the recipe level by default (
masterGain).
Read and change it with useSound():
const { play, muted, setMuted, masterGain, setMasterGain } = useSound();
play("notify");Haptics
On a phone that can vibrate, the tokens that mark a moment also buzz: select a short tick when a
choice changes, notify and celebrate a success, caution a warning, error an error (the
Haptic column above). A tap or a press never buzzes. Haptics don't wait for sound to be turned on,
stay still in a hidden tab, and never start before the person has touched the page.
const { play, buzz } = useSound();
play("select"); // a tick, and the sound when it's on
play("press", { haptic: "warn" }); // another haptic, or false for none
buzz("success"); // a haptic on its own<SoundProvider haptics={false}> turns them off. Android browsers vibrate; iPhones can't be made to
from a web page, so they stay still.
useTactile
useTactile binds motion and sound to the phases of an interaction, so a custom control feels like
the built-in ones:
import { useTactile } from "echo-ui-core/tactile";
const { ref, handlers } = useTactile<HTMLButtonElement>({
press: { motion: "press", sound: "press" },
release: { motion: "pop", sound: "release" },
});
return <button ref={ref} {...handlers}>Record</button>;- Phases:
press,release,hover,leave,focus,blur,activate. - Keyboard parity: Space and Enter give the same press and release as a pointer.
- Reduced motion is applied per token, as above; sound follows the provider.
- Haptics come with a phase's sound;
hapticnames another, orfalsefor none. - Escape hatch: pass raw GSAP vars or a recipe object instead of a token name.
For Base UI popups that need a GSAP exit before unmounting, use
useGsapPresence.
Don't
- Don't put a soft ease on anything on a screen. LCD and dot-matrix surfaces take
glitch. - Don't invent a duration inline. If the tokens do not cover a gesture, add a token.
- Don't set
transformin CSS on an elementuseTactileanimates — GSAP owns it. - Don't gate sound on reduced motion yourself; the provider already does.
- Don't add reverb or long tails to a recipe. A device, not an instrument.