beta
Timeline Scrubber <mv-timeline>
Régua de tempo de editor de vídeo com uma agulha de reprodução precisa ao frame que mostra o timecode durante o arraste, uma leitura ao passar o mouse, zoom em uma escala de meia oitava (botões, teclas, Ctrl/⌘ + roda do mouse ou pinça), uma faixa de marcadores, alças de entrada/saída que cobrem com um véu tudo o que está fora do intervalo, clipes em trilhas e uma barra de visão geral. Cada parte funciona pelo teclado. Integrado a formulários.
Categoria Formulários Tipo Web Component (<mv-timeline>) Status beta Keywords safe-rewrite, timeline, playhead, scrubber, timecode, video, zoom, markers, in-out, editor, form-associated, keyboard
When to use
A video, audio or animation editor needs a frame-accurate playhead with a timecode Users set in and out points, jump between cuts and markers and zoom along a time ruler, by pointer or keyboard Clips must be shown on tracks with play, pause and time events for syncing media
Avoid when
The need is a simple media progress bar with seeking → use Slider instead The goal is to show past events or project milestones as a list → use Stepper instead Users browse the edit history of a form rather than media time → use Rewind instead
Obtenha Timeline Scrubber. Incluído em todos os planos: o Pro pack traz todos os 317 componentes, com a CLI e o servidor MCP usados abaixo.
Ver planos
Instalação
node scripts/add.mjs timeline --out ./src/marvelous
Agente de IA com o servidor MCP do Marvelous UI: install_components({ slugs: ["timeline"], target_dir: "<absolute path>/src/marvelous", framework: "react" }).
Arquivos copiados (dependências incluídas): tokens/tokens.css, core/base.css, core/dom.js, core/element.js, core/form.js, core/motion.js, core/observe.js, components/timeline/timeline.js, components/timeline/timeline.css.
Uso
Marcação de referência, para usar como ponto de partida e personalizar com atributos, data-* e variáveis CSS:
<div id="tl-demo">
<style>
#tl-demo { display: grid; grid-template-columns: minmax(0, 1fr) 14rem; gap: .75rem; width: min(100%, 52rem); margin: 0 auto; }
#tl-demo .tld-monitor { position: relative; aspect-ratio: 16 / 8; overflow: hidden; border-radius: var(--mv-radius-lg); background: oklch(0.16 0.02 270); border: 1px solid var(--mv-border); }
#tl-demo .tld-sky { position: absolute; inset: 0; background: linear-gradient(180deg, oklch(0.35 0.12 280), oklch(0.62 0.16 30)); }
#tl-demo .tld-sun { position: absolute; left: 50%; width: 5rem; height: 5rem; margin-left: -2.5rem; border-radius: 50%; background: radial-gradient(circle, oklch(0.95 0.1 90), oklch(0.8 0.17 60)); box-shadow: 0 0 60px oklch(0.85 0.15 70 / .6); }
#tl-demo .tld-hills { position: absolute; inset: auto -10% -30% -10%; height: 60%; border-radius: 50% 50% 0 0; background: oklch(0.22 0.05 280); }
#tl-demo .tld-title { position: absolute; left: 0; right: 0; top: 18%; text-align: center; color: #fff; font: 700 clamp(1rem, 3vw, 1.6rem)/1.1 var(--mv-font-sans); letter-spacing: -.01em; text-shadow: 0 2px 16px oklch(0 0 0 / .35); }
#tl-demo .tld-title small { display: block; margin-top: .35rem; font: 500 .7rem var(--mv-font-mono); letter-spacing: .12em; text-transform: uppercase; opacity: .8; }
#tl-demo .tld-tc { position: absolute; right: .6rem; bottom: .5rem; padding: .15rem .4rem; border-radius: 4px; background: oklch(0 0 0 / .5); color: #fff; font: 500 .65rem var(--mv-font-mono); }
#tl-demo .tld-panel { display: grid; align-content: start; gap: .45rem; padding: .75rem; border: 1px solid var(--mv-border); border-radius: var(--mv-radius-lg); background: var(--mv-surface); font-size: .72rem; color: var(--mv-fg-muted); }
#tl-demo .tld-head { margin: 0 0 .15rem; font: 600 .7rem var(--mv-font-sans); letter-spacing: .06em; text-transform: uppercase; color: var(--mv-fg-subtle); }
#tl-demo .tld-row { display: flex; justify-content: space-between; gap: .5rem; }
#tl-demo .tld-row b { color: var(--mv-fg); font: 600 .72rem var(--mv-font-mono); font-variant-numeric: tabular-nums; }
#tl-demo .tld-hint { margin: .3rem 0 0; line-height: 1.5; }
#tl-demo kbd { font: 500 .62rem var(--mv-font-mono); padding: 0 .25rem; border-radius: 3px; background: var(--mv-bg-muted); border: 1px solid var(--mv-border); }
#tl-demo mv-timeline { grid-column: 1 / -1; }
@media (max-width: 640px) { #tl-demo { grid-template-columns: 1fr; } }
</style>
<div class="tld-monitor" aria-hidden="true">
<div class="tld-sky" id="tl-demo-sky"></div>
<div class="tld-sun" id="tl-demo-sun"></div>
<div class="tld-hills"></div>
<div class="tld-title" id="tl-demo-title">Sunrise<small>A film by Ines Okafor</small></div>
<span class="tld-tc" id="tl-demo-tc">00:00:00</span>
</div>
<div class="tld-panel" role="group" aria-label="Inspector">
<p class="tld-head">Inspector</p>
<div class="tld-row"><span>Shot</span><b id="tl-demo-clip">-</b></div>
<div class="tld-row"><span>Frame</span><b id="tl-demo-frame">0</b></div>
<div class="tld-row"><span>Sun (Y)</span><b id="tl-demo-y">-</b></div>
<div class="tld-row"><span>Title (opacity)</span><b id="tl-demo-op">-</b></div>
<div class="tld-row"><span>Range</span><b id="tl-demo-range">-</b></div>
<p class="tld-hint"><kbd>Space</kbd> play, <kbd>←</kbd>/<kbd>→</kbd> one frame (<kbd>Shift</kbd> ×10), <kbd>↑</kbd>/<kbd>↓</kbd> next cut or marker, <kbd>I</kbd>/<kbd>O</kbd> in/out, <kbd>+</kbd>/<kbd>−</kbd> or <kbd>Ctrl</kbd> + wheel to zoom.</p>
</div>
<mv-timeline id="tl-demo-timeline" name="time" label="Edit “Sunrise”" duration="24" fps="25" value="3.2" in="2" out="20" loop markers="6:Title, 12.5:Zenith, 18:Credits">
<div data-clip data-start="0" data-end="9.5" data-track="0">Wide shot · dawn</div>
<div data-clip data-start="9.5" data-end="16" data-track="0" style="--mv-timeline-clip: var(--mv-info)">Close-up</div>
<div data-clip data-start="16" data-end="24" data-track="0" style="--mv-timeline-clip: var(--mv-success)">Credits</div>
<div data-clip data-start="5" data-end="11" data-track="1" style="--mv-timeline-clip: var(--mv-warning)">Title</div>
</mv-timeline>
</div>
<script type="module">
const $ = (id) => document.getElementById(`tl-demo-${id}`);
const tl = $("timeline");
const ease = (x) => 1 - (1 - x) ** 3;
const clamp01 = (x) => Math.min(1, Math.max(0, x));
const render = () => {
const t = tl.value;
const rise = ease(clamp01(t / 12.5));
const y = 75 - rise * 55;
$("sun").style.top = `${y}%`;
$("sky").style.filter = `brightness(${0.55 + rise * 0.6}) saturate(${0.8 + rise * 0.4})`;
const op = clamp01((t - 5) / 1) * clamp01((11 - t) / 1);
$("title").style.opacity = op;
$("title").style.translate = `0 ${(1 - op) * 10}px`;
$("tc").textContent = tl.timecode;
$("frame").textContent = tl.frame;
$("y").textContent = `${y.toFixed(1)}%`;
$("op").textContent = `${Math.round(op * 100)}%`;
const clip = [...tl.querySelectorAll("[data-clip][data-track='0']")].find((c) => t >= +c.dataset.start && t < +c.dataset.end);
$("clip").textContent = clip?.textContent ?? "-";
const r = tl.range;
$("range").textContent = r ? `${r.in.toFixed(2)} → ${r.out.toFixed(2)} s` : "-";
};
for (const t of ["mv-input", "mv-timeupdate", "mv-change", "mv-range"]) tl.addEventListener(t, render);
customElements.whenDefined("mv-timeline").then(() => requestAnimationFrame(render));
</script>
API
Attributes
Name Tipo Default Description valuenumber 0Initial time in seconds (snapped to the frame). Restored on reset. durationnumber 60Total duration in seconds. fpsnumber 30Frames per second: playhead step and the “ff” of the timecode. in / outnumber Playback range (seconds). Draggable, focusable handles on the ruler; I and O keys; a clear button in the toolbar. markersstring “6:Title, 12.5:Zenith”: time and label, shown in their own lane, clickable and reachable with the keyboard. loopboolean Playback loops within the range (or the full duration). ratenumber 1Playback rate. labelstring Group name. name / disabled / readonlyStandard behavior; readonly allows playback but not range editing.
Properties
Name Tipo Description value / currentTimenumber Current time (s). frame / timecodenumber / string Frame number and “mm:ss:ff”. playingboolean Whether playback is running. range{ in, out } | null In/out range. zoomnumber Pixels per second (read/write). Zoom moves on quarter steps of a ×2 ladder above “fit”, so a written value snaps to the nearest step. stringsobject Overrides for the visible and spoken labels (play, pause, playhead, setIn, setOut, clear, inPoint, outPoint, zoomIn, zoomOut, fit, markers, marker, frame, help, group).
Methods
Name Description play() / pause() / toggle()Playback. It keeps running with reduced motion (it is content, not decoration) and pauses while the tab is hidden. setRange(in, out)Sets the range (or clears it with null). timecode(seconds, fps)Module export: mm:ss:ff formatting.
Events
Name Description mv-inputWhile dragging or on a keyboard step. detail: { time, frame, timecode }. mv-changeOn release, keyboard step, marker click and pause. mv-timeupdateOn every frame during playback. mv-play / mv-pausePlayback start and stop. mv-range / mv-zoomRange changed ({ range }); zoom changed ({ zoom, ratio }, ratio = scale relative to fit).
Content structure
Name Description [data-clip]Clips positioned by data-start / data-end (s) on track data-track (0, 1…). Color via --mv-timeline-clip.
CSS classes
Name Description mv-timeline-toolbar / -ruler / -markers / -marker / -tracks / -clip / -playhead / -playhead-tip / -range / -range-handle / -hover / -scrollbar / -thumbGenerated parts.
CSS variables
Name Default Description --mv-timeline-playheadvar(--mv-accent)Playhead color. --mv-timeline-markervar(--mv-warning)Marker color. --mv-timeline-clipvar(--mv-accent)Clip color (set on the [data-clip] element).
Accessibility
Named role="group". The playhead is a focusable role="slider" (aria-valuetext “00:03:05, frame 80”) described by a hidden shortcut summary; its timecode tip appears on focus and while scrubbing. Keyboard: ←/→ one frame (Shift ×10), PageUp/PageDown ±1 s, ↑/↓ next or previous cut, marker or range bound, Home/End to the in/out points then the ends, Space or K to play/pause, I/O to set in/out, +/− to zoom and 0 to fit. The in and out handles are sliders too (arrows, Page keys, Home/End, Delete clears the range). Markers are one tab stop with arrow-key roving; Enter jumps to the marker. Toolbar buttons are labeled (play uses aria-pressed), labels are overridable through strings. Escape while dragging restores the starting time or range; Shift while dragging snaps to markers, cuts and range bounds. The hover readout needs a precise pointer and never hides the system cursor. Forced colors use system colors.