Miraiclip SDK
React components

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.

Loading demo…
Press Play. The preview fits the composition's 16:9 frame inside the grey box.

Example

EditorPreview.tsx
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

ModeHowUse it when
FitLeave out width and heightThe 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
FixedPass both width and heightYou 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

PropDefaultDescription
width, heightFixed CSS size. Give both, or neither for fit mode
fitPadding0Fit mode: space kept free inside the parent, px
canvasPropsProps for the <canvas> element
childrenOverlays: 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:

FieldDescription
width, heightCSS size of the canvas
scaleScreen 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)
lastErrorThe last renderer or media error, as text, or null
Data attributeValue
data-modeThe mode above
data-errorPresent 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.

On this page