# Controlling playback (/docs/guides/controlling-playback)

> Drive one transition from buttons, a slider, scroll position or your own timeline.

VisDelta has no built-in controls. Every control calls the same controller
methods: `progress()`, `play()` and `pause()`. The player on this site is built
exactly this way.

<LiveExample id="bar-sort" code={false} />

## Buttons [#buttons]

```ts
const change = await transition(from, to, { target: '#chart' });

playButton.addEventListener('click', () => change.play({ duration: 800 }));
reverseButton.addEventListener('click', () => change.play({ from: 1, to: 0 }));
pauseButton.addEventListener('click', () => change.pause());
```

On a pair controller, `play()` starts from `0` unless you pass `from`. To resume
from the current frame, pass it explicitly:

```ts
resumeButton.addEventListener('click', () => {
  change.play({ from: change.value, to: 1 });
});
```

## A slider [#a-slider]

```ts
slider.addEventListener('input', () => {
  change.progress(slider.valueAsNumber); // 0 … 1
});
```

`progress()` pauses any running playback, so a reader can grab the slider
mid-animation.

## Scroll position [#scroll-position]

Use the [tested, single-file scroll story](/docs/guides/scroll-template) for
step cards, holds and replay. It measures actual card positions, coalesces events
and includes explicit handling for content-height iframes. Mapping progress from
an iframe's own viewport cannot reveal a cross-origin parent's scroll position.

All control modes call the same `progress()` API, so reverse scrolling needs no
separate chart animation.

## Your own clock [#your-own-clock]

Because progress is just a number, VisDelta can follow any timeline, such as a
media element, an animation library or a test harness:

```ts
video.addEventListener('timeupdate', () => {
  change.progress(video.currentTime / video.duration);
});
```

## Reflecting progress in the UI [#reflecting-progress-in-the-ui]

`play()` animates internally. Read `change.value` on each animation frame to keep
a slider or label in sync while it plays:

```ts
function playAndTrack(to: number) {
  change.play({ from: change.value, to });
  const tick = () => {
    slider.value = String(change.value);
    if (change.value !== to) requestAnimationFrame(tick);
  };
  requestAnimationFrame(tick);
}
```

## Respecting reduced motion [#respecting-reduced-motion]

Don't start animations automatically for readers who prefer reduced motion. A
reader-initiated control is still fine, as is jumping straight to the end state:

```ts
const reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
if (reduce) change.progress(1);
else change.play();
```
