Skip to content

Repository files navigation

Shotstack Studio

npm version License TypeScript

A JavaScript SDK for browser-based video editing with timeline, canvas preview, and export.

Interactive Examples

Try Shotstack Studio in your preferred framework:

TypeScript React Vue Angular Next.js

Features

  • Template-driven editing with undo/redo command model
  • Canvas preview rendering
  • Visual timeline with drag, resize, selection, and snapping
  • Extensible UI via UIController button API
  • Browser export pipeline via VideoExporter

Installation

npm install @shotstack/shotstack-studio
yarn add @shotstack/shotstack-studio

Quick Start

import { Edit, Canvas, Controls, Timeline, UIController } from "@shotstack/shotstack-studio";

// 1) Load a template
const response = await fetch("https://shotstack-assets.s3.amazonaws.com/templates/hello-world/hello.json");
const template = await response.json();

// 2) Create core components
const edit = new Edit(template);
const canvas = new Canvas(edit);
const ui = UIController.create(edit, canvas);

// 3) Load canvas and edit
await canvas.load();
await edit.load();

// 4) Register toolbar buttons
ui.registerButton({
  id: "text",
  icon: `<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M3 3H13"/><path d="M8 3V13"/><path d="M5 13H11"/></svg>`,
  tooltip: "Add Text"
});

// 5) Handle button clicks
ui.on("button:text", ({ position }) => {
  edit.addTrack(0, {
    clips: [
      {
        asset: {
          type: "rich-text",
          text: "Title",
          font: { family: "Work Sans", size: 72, weight: 600, color: "#ffffff", opacity: 1 },
          align: { horizontal: "center", vertical: "middle" }
        },
        start: position,
        length: 5,
        width: 500,
        height: 200
      }
    ]
  });
});

// 6) Initialize the Timeline
const timelineContainer = document.querySelector("[data-shotstack-timeline]") as HTMLElement;
const timeline = new Timeline(edit, timelineContainer, { resizable: true });
await timeline.load();

// 7) Add keyboard controls
const controls = new Controls(edit);
await controls.load();

// 8) Add event handlers
edit.events.on("clip:selected", data => {
  console.log("Clip selected:", data);
});

Your HTML must include both containers:

<div data-shotstack-studio></div>
<div data-shotstack-timeline></div>

Main Components

Edit

Edit is the runtime editing session and source of truth for document mutations.

import { Edit } from "@shotstack/shotstack-studio";

const edit = new Edit(templateJson);
await edit.load();

await edit.loadEdit(nextTemplateJson);

// Playback (seconds)
edit.play();
edit.pause();
edit.seek(2);
edit.stop();

// Mutations
await edit.addTrack(0, { clips: [] });
await edit.addClip(0, {
  asset: { type: "image", src: "https://example.com/image.jpg" },
  start: 0,
  length: 5
});
await edit.updateClip(0, 0, { length: 6 });
await edit.deleteClip(0, 0);

// History
await edit.undo();
await edit.redo();

// Clip operations
await edit.deleteTrack(0);

// Output settings
await edit.setOutputSize(1920, 1080);
await edit.setOutputFps(30);
await edit.setOutputFormat("mp4");
await edit.setOutputResolution("hd");
await edit.setOutputAspectRatio("16:9");
await edit.setTimelineBackground("#000000");

// Read state
const time = edit.playbackTime;
const playing = edit.isPlaying;
const clip = edit.getClip(0, 0);
const track = edit.getTrack(0);
const snapshot = edit.getEdit();
const durationSeconds = edit.totalDuration;

Events

Listen using string event names:

const unsubscribeClipSelected = edit.events.on("clip:selected", data => {
  console.log("Selected clip", data.trackIndex, data.clipIndex);
});

edit.events.on("clip:updated", data => {
  console.log("Updated from", data.previous, "to", data.current);
});

edit.events.on("playback:play", () => {
  console.log("Playback started");
});

// Unsubscribe when no longer needed
unsubscribeClipSelected();

Available event names:

Category Event Names
Playback playback:play, playback:pause
Timeline timeline:updated, timeline:backgroundChanged, timeline:resized
Clip lifecycle clip:added, clip:selected, clip:updated, clip:deleted, clip:restored, clip:copied, clip:loadFailed, clip:unresolved
Selection selection:cleared
Edit state edit:changed, edit:undo, edit:redo
Track track:added, track:removed
Duration duration:changed
Output output:resized, output:resolutionChanged, output:aspectRatioChanged, output:fpsChanged, output:formatChanged, output:destinationsChanged
Merge fields mergefield:changed

Generating assets from prompts

An image, video or audio asset can carry a prompt. Rendering generates from the prompt; src, when present, is the editor preview. Register a generator and the editor offers a generate action in the toolbar's generate pane:

edit.registerAssetGenerator(async ({ clipId, asset, signal }) => {
	const url = await myBackend.generate(asset, { signal });
	return { url };
});

Pass a model catalogue as the registration's catalogue option to show model and option controls. Entries must include their option schema; those without one are ignored. The Edit API returns this shape from GET /models?expand=options. The SDK stores a snapshot; fetching and refreshing it remain the host's responsibility.

The SDK writes the returned URL to the clip, so the change is undoable and autosaves like any other edit. It tracks whether a clip is generating or has failed, and renders those states; a rejection's message is shown as-is next to a retry action. Everything else — which models exist, what they cost, what an error means — stays with the host.

Generation state is transient: it is never saved to the edit, never part of undo, and gone on reload. Deleting a clip mid-generation aborts its request via the signal.

A prompt is the only thing that makes a clip generate. Add one to a plain image, video or audio asset to make it generative; clear it and the clip keeps its current src and stops generating, which is how you hold on to a result you want.

Generation is content-addressed, so the same prompt, model and options resolve to the same asset on every render. A generator that does not go through Shotstack's own generation hands back a preview that the render replaces.

Canvas

Canvas renders the current edit.

import { Canvas } from "@shotstack/shotstack-studio";

const canvas = new Canvas(edit);
await canvas.load();

canvas.centerEdit();
canvas.zoomToFit();
canvas.setZoom(1.25);
canvas.resize();
const zoom = canvas.getZoom();
canvas.dispose();

UIController

UIController manages built-in UI wiring and extensible button events.

import { UIController } from "@shotstack/shotstack-studio";

const ui = UIController.create(edit, canvas, { mergeFields: true });

ui.registerButton({
  id: "add-title",
  icon: `<svg viewBox="0 0 16 16">...</svg>`,
  tooltip: "Add Title"
});

const unsubscribe = ui.on("button:add-title", ({ position }) => {
  console.log("Button clicked at", position, "seconds");
});

ui.unregisterButton("add-title");
unsubscribe();
ui.dispose();

Timeline

Timeline provides visual clip editing.

The container must have an explicit CSS height (e.g. height: 300px) and overflow: hidden. Avoid flex-grow or !important on height — the resize handle sets height via inline style.

import { Timeline } from "@shotstack/shotstack-studio";

const container = document.querySelector("[data-shotstack-timeline]") as HTMLElement;
const timeline = new Timeline(edit, container, { resizable: true });

await timeline.load();
timeline.zoomIn();
timeline.zoomOut();
timeline.dispose();

Pass { resizable: false } to hide the drag handle. When enabled (default), a timeline:resized event fires with { height } after the user finishes dragging or double-clicks to reset.

Controls

Controls enables keyboard playback/edit shortcuts.

import { Controls } from "@shotstack/shotstack-studio";

const controls = new Controls(edit);
await controls.load();

VideoExporter

VideoExporter exports a timeline render from the browser runtime.

import { VideoExporter } from "@shotstack/shotstack-studio";

const exporter = new VideoExporter(edit, canvas);
await exporter.export("my-video.mp4", 25);

Merge Fields

Merge fields are template placeholders, typically in the form {{ FIELD_NAME }}.

{
  "asset": {
    "type": "text",
    "text": "{{ TITLE }}"
  }
}

When merge-field-aware UI is required, enable it via UIController options:

const ui = UIController.create(edit, canvas, { mergeFields: true });

You can also subscribe to merge field events when integrations update merge data:

edit.events.on("mergefield:changed", ({ fields }) => {
  console.log("Merge fields updated:", fields.length);
});

Custom UI Buttons

Use UIController to register and handle custom button actions.

ui.registerButton({
  id: "text",
  icon: `<svg viewBox="0 0 16 16">...</svg>`,
  tooltip: "Add Text",
  dividerBefore: true
});

ui.on("button:text", ({ position, selectedClip }) => {
  console.log("Current time (seconds):", position);
  console.log("Current selection:", selectedClip);
});

ui.unregisterButton("text");

API Reference

For schema-level details and type definitions, see the Shotstack API Reference.

License

PolyForm Shield License 1.0.0

About

A JavaScript library for creating and editing videos in the browser.

Resources

Stars

56 stars

Watchers

2 watching

Forks

Releases

Used by

Contributors

Languages