Preview
The preview canvas, wired to playback, with overlays on top.
<Preview> renders the <canvas> the renderer draws into. It hands the canvas to PlaybackProvider on mount, keeps the render target matched to its size, and releases it on unmount. Anything you put inside it is drawn over the canvas.
Example
import { Preview, usePlayback } from "@miraiclip/react";
// Inside <ProjectProvider> and <PlaybackProvider>.
export function EditorPreview() {
return (
// No width/height on <Preview>: it fits the composition's aspect inside this box.
<div className="flex h-80 items-center justify-center bg-neutral-900">
<Preview fitPadding={16} className="overflow-hidden rounded-lg bg-black shadow-lg">
{({ mode, lastError }) => (
<>
{mode === "unsupported" && (
<p className="absolute inset-0 grid place-items-center p-4 text-center text-sm text-white/80">
Live preview needs WebCodecs (Chrome, Edge or Safari 17+).
</p>
)}
{lastError && (
<p className="absolute bottom-2 left-2 rounded bg-red-600/90 px-2 py-0.5 text-xs text-white">{lastError}</p>
)}
</>
)}
</Preview>
</div>
);
}
export function PlayButton() {
const { isPlaying, toggle } = usePlayback();
return (
<button type="button" onClick={toggle} className="rounded-md bg-sky-500 px-3 py-1 text-sm font-medium text-white">
{isPlaying ? "Pause" : "Play"}
</button>
);
}Sizing
| Mode | How | Use it when |
|---|---|---|
| Fit | Leave out width and height | The preview should fill an area. It measures its parent element (live, with a ResizeObserver) and fits the composition's aspect inside it, minus fitPadding on each side |
| Fixed | Pass both width and height | You size it yourself, for example from a zoom control |
Sizes are CSS pixels. The renderer draws at that size times devicePixelRatio (capped at 2), and decodes no larger than it shows. In fit mode, give the parent a size that doesn't depend on the preview (a fixed height, or flex-1 in a sized column), and use fitPadding instead of padding on the parent.
Props
| Prop | Default | Description |
|---|---|---|
width, height | Fixed CSS size. Give both, or neither for fit mode | |
fitPadding | 0 | Fit mode: space kept free inside the parent, px |
canvasProps | Props for the <canvas> element | |
children | Overlays: nodes, or (state) => node |
The element is position: relative and exactly the canvas's size, so overlays can use position: absolute; inset: 0. Keep overlays as DOM or SVG on top of the canvas: they never reach an export.
State
The children function and usePreview() (inside <Preview>) receive:
| Field | Description |
|---|---|
width, height | CSS size of the canvas |
scale | Screen pixels per composition pixel. Multiply composition coordinates by it to place overlays |
mode | "live" (the renderer is drawing), "fallback" (no renderer yet, or it failed: a clock still moves the playhead) or "unsupported" (no WebCodecs) |
lastError | The last renderer or media error, as text, or null |
| Data attribute | Value |
|---|---|
data-mode | The mode above |
data-error | Present after a render or media error |
Playback controls
<Preview> has no controls. Build them from usePlayback(): isPlaying, toggle(), seek(us), volume, setVolume(v) and toggleMute() (see React › Preview and playback). The PlayButton in the example above is one; <Scrubber> is a seek bar.
Overlays
For selection handles on the canvas, use <TransformBox>. For your own overlays, convert composition pixels with scale, and use getClipBounds(id) and hitTest(x, y) from usePlayback() to find where clips are drawn:
import { usePlayback, usePreview, useSelection } from "@miraiclip/react";
/** A dot at the selected clip's center. Render it inside <Preview>. */
function SelectionDot() {
const { scale } = usePreview();
const { getClipBounds } = usePlayback();
const [id] = useSelection();
const bounds = id ? getClipBounds(id) : null;
if (!bounds) return null;
return (
<div
className="absolute h-2 w-2 -translate-x-1/2 -translate-y-1/2 rounded-full bg-sky-400"
style={{ left: bounds.centerXPx * scale, top: bounds.centerYPx * scale }}
/>
);
}getClipBounds reads the current frame, so it is fresh on each render, but nothing re-renders this component when playback or keyframes move the clip. <TransformBox.Root> follows the clip every frame for that reason.
Without the component
<Preview> is a thin wrapper over attachCanvas, resizePreview and detachCanvas from usePlayback(). Build an editor › Preview & playback shows the same with the framework-free preview controller.