Exclusivebeta
Still Time — <mv-still-time>
Ambient-motion governor: wrap any region and every animation inside it (CSS keyframes, CSS transitions, Web Animations, muted videos) runs only as fast as the user is acting. Pointer speed, wheel and scroll speed, key presses and touch moves feed an activity level that a critically damped spring turns into a smooth time rate, from a near-freeze when you are still (×0.03 by default, or an exact stop with idle-freeze) to full speed under the hand (×1). The rate is applied through playbackRate without restarting anything, exposed as --mv-time-rate, a rate property, a scaled time clock and a throttled mv-timerate event so canvas and shader loops can follow, and the governor sleeps completely at rest. Calm by default, alive under the hand.
| Category | Backgrounds |
|---|---|
| Type | Web Component (<mv-still-time>) |
| Status | beta |
| Keywords | exclusive, culture, ambient, motion, playback-rate, time-scale, slow-motion, activity, idle, calm, governor, web-animations, css-animations, background, hero, attention |
When to use
- A landing hero or dashboard has decorative motion that competes with reading and should calm down while visitors read
- Ambient loops (marquees, orbits, floating cards, shimmer) should feel responsive without distracting idle users
- A kiosk or wall display should stay nearly still until someone interacts with it
- Canvas or shader backgrounds need a shared speed signal tied to user activity
Avoid when
- The animation conveys real progress or status (a loading spinner, a countdown) that must never slow down: mark it data-still-time-ignore or keep it outside
- The goal is to cap the total amount of motion on a page for performance or accessibility → use Motion Budget instead
- Motion should simply follow scroll position rather than activity → use Scroll Velocity instead
Install
node scripts/add.mjs still-time --out ./src/marvelousAI agent with the Marvelous UI MCP server: install_components({ slugs: ["still-time"], target_dir: "<absolute path>/src/marvelous", framework: "react" }).
Files copied (dependencies included): tokens/tokens.css, core/base.css, core/dom.js, core/element.js, core/motion.js, components/still-time/still-time.js, components/still-time/still-time.css.
Usage
Canonical markup — start from it and customize with attributes, data-* and CSS variables:
<div id="st-demo">
<style>
#st-demo { display:grid; gap:1rem; width:100%; max-width:68rem; margin-inline:auto; font-size:.875rem }
#st-demo .st-hero { position:relative; overflow:hidden; display:grid; grid-template-columns:minmax(0,1fr) minmax(0,1fr); align-items:center; gap:2rem; min-height:27rem; padding:2.5rem 2.5rem 5.5rem; border:1px solid var(--mv-border); border-radius:var(--mv-radius-2xl); background:var(--mv-surface); box-shadow:var(--mv-shadow-sm); isolation:isolate }
#st-demo .st-sky { position:absolute; inset:0; z-index:-1; pointer-events:none; overflow:hidden }
#st-demo .st-blob { position:absolute; width:26rem; height:26rem; border-radius:50%; opacity:.55; background:radial-gradient(closest-side, color-mix(in oklch, var(--_c) 55%, transparent), transparent 70%); animation:st-drift 18s ease-in-out infinite alternate }
#st-demo .st-b1 { --_c:var(--mv-accent); left:-6rem; top:-8rem }
#st-demo .st-b2 { --_c:var(--mv-info); right:-4rem; top:-6rem; animation-duration:22s; animation-delay:-7s }
#st-demo .st-b3 { --_c:var(--mv-success); right:18%; bottom:-14rem; animation-duration:26s; animation-delay:-13s; opacity:.35 }
@keyframes st-drift { to { transform:translate(4rem, 3rem) scale(1.15) } }
#st-demo .st-copy { display:grid; gap:1rem; justify-items:start; max-width:28rem }
#st-demo .st-copy h2 { margin:0; font-size:clamp(1.75rem, 1.2rem + 1.8vw, 2.5rem); line-height:1.08; letter-spacing:-.03em; font-weight:650 }
#st-demo .st-copy p { margin:0; color:var(--mv-fg-muted); font-size:.9375rem; line-height:1.55 }
#st-demo .st-cta { display:flex; flex-wrap:wrap; gap:.5rem; margin-top:.25rem }
#st-demo .st-visual { position:relative; height:21rem }
#st-demo .st-orbit { position:absolute; left:50%; top:50%; width:15rem; height:15rem; margin:-7.5rem 0 0 -7.5rem }
#st-demo .st-ring { position:absolute; inset:0; border-radius:50%; border:1px dashed color-mix(in oklch, var(--mv-fg) 16%, transparent) }
#st-demo .st-ring-2 { inset:2.5rem; border-style:solid; border-color:color-mix(in oklch, var(--mv-accent) 22%, transparent) }
#st-demo .st-spin { position:absolute; inset:0; animation:st-rotate 16s linear infinite }
#st-demo .st-spin-2 { inset:2.5rem; animation-duration:9s; animation-direction:reverse }
#st-demo .st-dot { position:absolute; left:50%; top:0; width:.75rem; height:.75rem; margin:-.375rem 0 0 -.375rem; border-radius:50%; background:var(--mv-accent); box-shadow:0 0 0 4px color-mix(in oklch, var(--mv-accent) 18%, transparent); transform-origin:50% 7.875rem }
#st-demo .st-spin-2 .st-dot { width:.5rem; height:.5rem; margin:-.25rem 0 0 -.25rem; transform-origin:50% 5.25rem; background:var(--mv-fg-muted); box-shadow:none }
#st-demo .st-mark { position:absolute; left:50%; top:50%; display:grid; place-items:center; width:4rem; height:4rem; margin:-2rem 0 0 -2rem; border-radius:1.25rem; color:var(--mv-fg-on-accent); background:linear-gradient(145deg, color-mix(in oklch, var(--mv-accent), white 18%), color-mix(in oklch, var(--mv-accent), black 18%)); box-shadow:0 12px 30px -10px color-mix(in oklch, var(--mv-accent) 70%, transparent); animation:st-breathe 4s ease-in-out infinite }
#st-demo .st-mark svg { width:1.75rem; height:1.75rem }
@keyframes st-rotate { to { rotate:360deg } }
@keyframes st-breathe { 50% { scale:1.06 } }
#st-demo .st-card { position:absolute; display:grid; gap:.375rem; margin:0; padding:.75rem .875rem; min-width:11.5rem; border:1px solid var(--mv-border); border-radius:var(--mv-radius-xl); background:color-mix(in oklch, var(--mv-surface-raised) 88%, transparent); -webkit-backdrop-filter:blur(10px); backdrop-filter:blur(10px); box-shadow:var(--mv-shadow-lg); animation:st-float 6s ease-in-out infinite alternate }
#st-demo .st-card small { color:var(--mv-fg-muted); font-size:.6875rem; font-weight:500; text-transform:uppercase; letter-spacing:.06em }
#st-demo .st-card b { font-size:1.125rem; letter-spacing:-.02em; font-variant-numeric:tabular-nums }
#st-demo .st-card .st-up { color:var(--mv-success); font-size:.75rem; font-weight:600 }
#st-demo .st-row { display:flex; align-items:center; gap:.5rem; font-size:.8125rem; font-weight:500 }
#st-demo .st-row span:last-child { color:var(--mv-fg-muted); font-weight:400; margin-inline-start:auto; font-size:.75rem }
#st-demo .st-c1 { left:0; top:1rem; animation-delay:-2s }
#st-demo .st-c2 { right:0; bottom:4rem; animation-duration:7.5s; animation-delay:-4s }
#st-demo .st-c3 { left:4%; bottom:.25rem; animation-duration:8.5s; animation-delay:-1s; min-width:13rem }
@keyframes st-float { from { transform:translateY(-7px) rotate(-.6deg) } to { transform:translateY(7px) rotate(.6deg) } }
#st-demo .st-spark { width:100%; height:1.75rem; overflow:visible }
#st-demo .st-spark path { fill:none; stroke:var(--mv-accent); stroke-width:2; stroke-linecap:round; stroke-linejoin:round; stroke-dasharray:120; animation:st-draw 5s var(--mv-ease-in-out) -3.4s infinite }
@keyframes st-draw { 0% { stroke-dashoffset:120 } 55%, 100% { stroke-dashoffset:0 } }
#st-demo .st-check { display:grid; place-items:center; width:1.25rem; height:1.25rem; border-radius:50%; background:color-mix(in oklch, var(--mv-success) 18%, transparent); color:var(--mv-success) }
#st-demo .st-check svg { width:.75rem; height:.75rem }
#st-demo .st-bar { position:relative; height:.375rem; border-radius:var(--mv-radius-full); background:var(--mv-bg-muted); overflow:hidden }
#st-demo .st-bar i { position:absolute; inset:0 38% 0 0; border-radius:inherit; background:linear-gradient(90deg, var(--mv-accent), color-mix(in oklch, var(--mv-accent), white 30%) 50%, var(--mv-accent)) 0 0 / 200% 100%; animation:st-shimmer 1.8s linear infinite }
@keyframes st-shimmer { to { background-position:-200% 0 } }
#st-demo .st-marquee { position:absolute; left:0; right:0; bottom:0; height:3.5rem; display:flex; align-items:center; overflow:hidden; border-top:1px solid var(--mv-border); background:color-mix(in oklch, var(--mv-bg-subtle) 70%, transparent); -webkit-mask:linear-gradient(90deg, transparent, #000 10%, #000 90%, transparent); mask:linear-gradient(90deg, transparent, #000 10%, #000 90%, transparent) }
#st-demo .st-track { display:flex; flex:none; gap:3rem; padding-inline-end:3rem; animation:st-scroll 28s linear infinite; color:var(--mv-fg-subtle); font-weight:600; letter-spacing:-.01em; white-space:nowrap }
#st-demo .st-track span { display:inline-flex; align-items:center; gap:.5rem }
#st-demo .st-track span::before { content:""; width:.625rem; height:.625rem; border-radius:.2rem; background:currentColor; opacity:.5 }
@keyframes st-scroll { to { transform:translateX(-100%) } }
#st-demo .st-lower { display:grid; grid-template-columns:minmax(0,1.1fr) minmax(0,1fr); gap:1rem }
#st-demo .st-panel { position:relative; display:grid; gap:1rem; padding:1.25rem 1.25rem 3.25rem; border:1px solid var(--mv-border); border-radius:var(--mv-radius-xl); background:var(--mv-surface); box-shadow:var(--mv-shadow-sm) }
#st-demo .st-panel header { display:flex; align-items:center; gap:.5rem }
#st-demo .st-panel header strong { font-size:.9375rem }
#st-demo .st-panel header span { margin-inline-start:auto; color:var(--mv-fg-muted); font-size:.8125rem; font-variant-numeric:tabular-nums }
#st-demo .st-live { position:relative; width:.5rem; height:.5rem; border-radius:50%; background:var(--mv-success) }
#st-demo .st-live::after { content:""; position:absolute; inset:0; border-radius:50%; background:var(--mv-success); animation:st-ping 1.6s var(--mv-ease-out) infinite }
@keyframes st-ping { to { transform:scale(3); opacity:0 } }
#st-demo .st-eq { display:flex; align-items:flex-end; gap:.375rem; height:6.5rem; padding-bottom:.25rem; border-bottom:1px solid var(--mv-border) }
#st-demo .st-eq i { flex:1; height:100%; border-radius:.3rem .3rem .1rem .1rem; background:linear-gradient(to top, color-mix(in oklch, var(--mv-accent) 70%, transparent), var(--mv-accent)); transform-origin:bottom; animation:st-eq 1.6s ease-in-out infinite alternate }
@keyframes st-eq { from { transform:scaleY(.18) } to { transform:scaleY(1) } }
#st-demo .st-legend { display:flex; justify-content:space-between; color:var(--mv-fg-subtle); font-size:.6875rem; font-variant-numeric:tabular-nums }
#st-demo .st-side { display:grid; gap:1rem; align-content:start; padding:1.25rem; border:1px solid var(--mv-border); border-radius:var(--mv-radius-xl); background:var(--mv-bg-subtle) }
#st-demo .st-side h3 { margin:0; font-size:.9375rem }
#st-demo .st-side p { margin:0; color:var(--mv-fg-muted); line-height:1.5 }
#st-demo .st-readouts { display:grid; gap:.5rem }
#st-demo .st-readout { display:flex; align-items:center; gap:.75rem; padding:.625rem .75rem; border:1px solid var(--mv-border); border-radius:var(--mv-radius-lg); background:var(--mv-surface) }
#st-demo .st-readout span { color:var(--mv-fg-muted) }
#st-demo .st-readout output { margin-inline-start:auto; font:600 .8125rem/1 var(--mv-font-mono); font-variant-numeric:tabular-nums }
#st-demo .st-readout output small { color:var(--mv-fg-subtle); font-weight:500 }
#st-demo .st-toggle { display:flex; align-items:center; gap:.625rem; cursor:pointer; font-weight:500 }
@media (prefers-reduced-motion: reduce) {
#st-demo .st-blob, #st-demo .st-spin, #st-demo .st-mark, #st-demo .st-card, #st-demo .st-spark path, #st-demo .st-bar i, #st-demo .st-track, #st-demo .st-live::after, #st-demo .st-eq i { animation:none }
}
:where([data-motion="reduce"]) #st-demo :is(.st-blob, .st-spin, .st-mark, .st-card, .st-spark path, .st-bar i, .st-track, .st-eq i), :where([data-motion="reduce"]) #st-demo .st-live::after { animation:none }
@media (max-width: 760px) {
#st-demo .st-hero { grid-template-columns:1fr; padding:1.5rem 1.25rem 5rem }
#st-demo .st-lower { grid-template-columns:1fr }
}
</style>
<mv-still-time class="st-hero" id="st-hero" indicator>
<div class="st-sky" aria-hidden="true"><span class="st-blob st-b1"></span><span class="st-blob st-b2"></span><span class="st-blob st-b3"></span></div>
<div class="st-copy">
<span class="mv-badge" data-variant="secondary">New · Focus mode</span>
<h2>Analytics that move when you do</h2>
<p>Your dashboard stays calm while you read and comes alive the moment you reach for it. Move the pointer over this panel.</p>
<div class="st-cta">
<button class="mv-button">Start free trial</button>
<button class="mv-button" data-variant="outline">Book a demo</button>
</div>
</div>
<div class="st-visual" aria-hidden="true">
<div class="st-orbit">
<span class="st-ring"></span>
<span class="st-ring st-ring-2"></span>
<span class="st-spin"><i class="st-dot" style="rotate:20deg"></i><i class="st-dot" style="rotate:140deg"></i><i class="st-dot" style="rotate:260deg"></i></span>
<span class="st-spin st-spin-2"><i class="st-dot" style="rotate:80deg"></i><i class="st-dot" style="rotate:260deg"></i></span>
<span class="st-mark"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 19V9M10 19V5M16 19v-7M22 19H2"/></svg></span>
</div>
<figure class="st-card st-c1">
<small>Revenue · this week</small>
<div class="st-row"><b>$48,210</b><span class="st-up">↑ 12.4%</span></div>
<svg class="st-spark" viewBox="0 0 120 28" preserveAspectRatio="none"><path d="M2 22 18 18 30 20 44 12 58 15 72 8 86 11 100 5 118 3"/></svg>
</figure>
<figure class="st-card st-c2">
<div class="st-row"><span class="st-check"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><path d="m5 12.5 4.5 4.5L19 7.5"/></svg></span>Deploy succeeded<span>42s</span></div>
<div class="st-row"><span class="mv-spinner" style="--mv-spinner-size:1rem;color:var(--mv-accent)"></span>Warming edge cache<span>3 of 5</span></div>
</figure>
<figure class="st-card st-c3">
<div class="st-row">Syncing 1,284 events<span>62%</span></div>
<div class="st-bar"><i></i></div>
</figure>
</div>
<div class="st-marquee" aria-hidden="true">
<div class="st-track"><span>Northwind</span><span>Acme Logistics</span><span>Halden Studio</span><span>Blue Harbor</span><span>Lumen Health</span><span>Vertex Labs</span><span>Orbital Freight</span></div>
<div class="st-track"><span>Northwind</span><span>Acme Logistics</span><span>Halden Studio</span><span>Blue Harbor</span><span>Lumen Health</span><span>Vertex Labs</span><span>Orbital Freight</span></div>
</div>
</mv-still-time>
<div class="st-lower">
<mv-still-time class="st-panel" id="st-panel" idle-freeze indicator>
<header><i class="st-live" aria-hidden="true"></i><strong>Live traffic</strong><span>1,284 visitors now</span></header>
<div class="st-eq" aria-hidden="true">
<i style="animation-delay:-.2s"></i><i style="animation-delay:-1.1s"></i><i style="animation-delay:-.6s"></i><i style="animation-delay:-1.4s"></i><i style="animation-delay:-.3s"></i><i style="animation-delay:-.9s"></i><i style="animation-delay:-1.5s"></i><i style="animation-delay:-.5s"></i><i style="animation-delay:-1.2s"></i><i style="animation-delay:-.1s"></i><i style="animation-delay:-.8s"></i><i style="animation-delay:-1.3s"></i><i style="animation-delay:-.4s"></i><i style="animation-delay:-1s"></i>
</div>
<div class="st-legend" aria-hidden="true"><span>2:00 PM</span><span>2:30 PM</span><span>3:00 PM</span></div>
</mv-still-time>
<div class="st-side">
<h3>Time follows your hand</h3>
<p>Keep still: the hero crawls at ×0.03 and the traffic panel freezes completely. Move, scroll or type and both speed up to ×1.00.</p>
<div class="st-readouts">
<div class="st-readout"><span>Hero</span><output id="st-hero-rate">×0.03 <small>→ ×1.00</small></output></div>
<div class="st-readout"><span>Traffic panel</span><output id="st-panel-rate">×0.00 <small>→ ×1.00</small></output></div>
</div>
<label class="st-toggle"><input type="checkbox" role="switch" class="mv-switch" id="st-freeze" checked> Freeze the panel completely when idle</label>
</div>
</div>
</div>
<script type="module">
const root = document.getElementById("st-demo");
if (root) {
const show = (el, out) => {
const paint = (rate) => {
out.textContent = `×${rate.toFixed(2)} `;
const max = document.createElement("small");
max.textContent = "→ ×1.00";
out.append(max);
};
el.addEventListener("mv-timerate", (e) => paint(e.detail.rate));
customElements.whenDefined("mv-still-time").then(() => paint(el.rate ?? 0));
};
const hero = root.querySelector("#st-hero");
const panel = root.querySelector("#st-panel");
show(hero, root.querySelector("#st-hero-rate"));
show(panel, root.querySelector("#st-panel-rate"));
root.querySelector("#st-freeze").addEventListener("change", (e) => {
panel.idleFreeze = e.target.checked;
});
}
</script>Cultural reference
SUPERHOT — SUPERHOT Team (2016, video game). Time moves only when you move: the world is frozen while you stand still. In the UI, ambient animations run only as fast as the user is acting and crawl to a near-freeze when the user is still.
API
Attributes
| Name | Type | Default | Description |
|---|---|---|---|
min-rate | number | 0.03 | Time rate when the user is still (0 … max-rate). Animations crawl at this speed instead of stopping, so the scene never looks broken. |
max-rate | number | 1 | Time rate under full activity. Values above 1 make motion faster than authored. |
idle-freeze | boolean | false | Exact 0 when idle: animations hold their current frame (playbackRate 0) and governed videos are paused, then resume where they were. |
scope | region | page | region | Where activity is measured: only over the element (pointer, touch, wheel, keys and focus inside it, scrolling of its descendants), or over the whole page (including window scroll speed). |
sensitivity | number | 1 | Multiplies pointer and scroll speeds before they are mapped to activity (2 = half the speed is needed to reach full time). |
video | muted | all | none | muted | Which <video> descendants follow the rate (clamped to the 0.0625–16 range browsers support). Muted only by default, so audio is never slowed down. |
indicator | boolean | false | Shows a small decorative time gauge in the bottom corner (ring on a log scale + “×0.03”), aria-hidden, plus a visually hidden sentence explaining the behavior. |
data-still-time-ignore | boolean (on a descendant) | Excludes this subtree: its animations and videos keep their own speed (use it for spinners, countdowns and other honest status). |
Properties
| Name | Type | Description |
|---|---|---|
rate | number | Current time rate (read-only). |
time | number (s) | Scaled clock: seconds that advance at the current rate (read-only). Drive a canvas or shader loop with it to follow the governor exactly. |
minRate, maxRate, idleFreeze, scope, sensitivity, video, indicator | reflected | Mirror the attributes (scope is read when the element connects). |
Methods
| Name | Description |
|---|---|
nudge(amount = 0.6) | Feeds activity manually (0 … 1), e.g. from a gamepad, a voice command or a custom gesture. |
Events
| Name | Description |
|---|---|
mv-timerate | Throttled to ~10 per second while the rate changes, plus one when it comes to rest. detail: { rate, activity, resting }. |
Content structure
| Name | Description |
|---|---|
(content) | Any content with ambient motion: CSS keyframes, transitions, Web Animations (other components included), muted <video>. Script-created animations are picked up within ~1.5 s, CSS ones as soon as they start. |
CSS classes
| Name | Description |
|---|---|
mv-still-time-gauge | Indicator pill (aria-hidden); data-level = frozen | still | moving | live. |
mv-still-time-arc / -track / -hand / -value | Parts of the gauge: filled ring, its track, the clock hand and the “×0.03” text. |
CSS variables
| Name | Default | Description |
|---|---|---|
--mv-time-rate | 1 | Set on the element to the current rate (3 decimals) while it changes: read it in calc() for effects that are not animations (blur, glow, transition-duration of your own). |
--mv-still-time-color | var(--mv-accent) | Color of the gauge arc. |
--mv-still-time-inset | var(--mv-space-3) | Distance of the gauge from the bottom and inline-end edges. |
Accessibility
Purely presentational: it adds no roles, no focus stops and never moves focus. It only slows motion down, so it reduces distraction for people who read slowly or are sensitive to motion, while keyboard users are treated like pointer users (key presses and focus changes count as activity, so nobody gets a frozen interface because they do not use a mouse). The indicator gauge is aria-hidden; when indicator is set, a visually hidden sentence explains that ambient animations follow your activity (no live announcements, which would be noise). Honest status animations (spinners, countdowns, progress) can be excluded with data-still-time-ignore. Reduced motion (system or data-motion="reduce"): the governor does not react at all and keeps the rate at min-rate (0 with idle-freeze); well-behaved content already stops its own continuous animations in that mode. Performance: the animation set is refreshed on animationstart / transitionrun events plus a 1.5 s sweep (getAnimations({ subtree: true })), never per frame; the frameLoop runs only while the rate is changing and sleeps at rest (no work at all while nobody moves), pauses offscreen and in hidden tabs; playbackRate is updated with updatePlaybackRate (no restart, no jump) only when it actually changed; video rates are updated at ~10 Hz. On disconnect every governed animation and video is restored to its natural rate and resumed if it was paused.