Screenplay Editor
Write structured screenplays with scene headings, dialogue, and toolbar commands.
Write scene headings, action, characters, dialogue, parentheticals, and transitions in a structured screenplay. Use the public JavaScript methods with your own controls or connect the existing Toolbar.
Try the editor
Click a block to edit it, then change its type or position with the toolbar. The active block has a black left marker and a six-dot drag handle: drag it above or below another block using the full-width insertion line (blank space beside a block accepts drops too), or focus the handle and use the Up/Down arrows. Moving a continuation reorders the entire semantic block and can be undone. Enter creates the next block; Shift+Enter inserts a line break. A character or parenthetical continues into dialogue. Backspace removes an empty block. Ctrl/Cmd+Z undoes; Ctrl/Cmd+Shift+Z redoes. Ordinary Tab moves between controls. Write on complete, numbered A4 sheets (210 × 297 mm), with 12-point Courier and fixed screenplay margins. Fit parent is the default preview size. Use the zoom menu for a fixed percentage; the writing canvas scrolls vertically and horizontally as needed. Preview size does not change line breaks or page count. Additional sheets appear as you write. The document title sits outside the numbered screenplay pages.
Changes stay in this browser session until exported.
A conventionally formatted screenplay page represents roughly one minute of screen time; this is a pacing guide, not an exact runtime calculation. See the Academy’s screenwriting guide. A4 and US Letter have different usable areas, and scene content also affects timing.
Setup
Initialize explicitly after the host is in the document. No additional editor dependency is required. AutoInit does not create this data-driven editor.
<div id="writing-toolbar" class="toolbar">
<div class="toolbar-group">
<crazy-button data-editor-command="type" data-editor-value="scene" type="floating" label="Scene heading" size="small" shape="round" depth="flat" icon-text="movie" icon-position="left" variant="tonal" color-secondary="var(--md-sys-color-primary)"></crazy-button>
<crazy-button data-editor-command="type" data-editor-value="dialogue" type="floating" label="Dialogue" size="small" shape="round" depth="flat" icon-text="chat_bubble_outline" icon-position="left" variant="tonal" color-secondary="var(--md-sys-color-primary)"></crazy-button>
<crazy-button data-editor-command="insert" data-editor-value="action" type="floating" label="Add action" size="small" shape="round" depth="flat" icon-text="notes" icon-position="left" variant="tonal" color-secondary="var(--md-sys-color-primary)"></crazy-button>
<crazy-button data-editor-command="undo" type="floating" label="Undo" size="small" shape="round" depth="flat" icon-text="undo" icon-position="left" variant="tonal" color-secondary="var(--md-sys-color-primary)"></crazy-button>
<crazy-button data-editor-command="redo" type="floating" label="Redo" size="small" shape="round" depth="flat" icon-text="redo" icon-position="left" variant="tonal" color-secondary="var(--md-sys-color-primary)"></crazy-button>
</div>
</div>
<div id="screenplay"></div>import { ScreenplayEditor, Toolbar } from "kmaterialize";
import "kmaterialize/dist/css/materialize.css";
// Create a structured screenplay.
const editor = ScreenplayEditor.init(document.querySelector("#screenplay"), {
document: {
title: "The last light",
blocks: [
{ id: "scene-1", type: "scene", text: "EXT. MOUNTAIN LAKE — DAWN" },
{ id: "action-1", type: "action", text: "Mist drifts across the water." }
]
}
});
// Initialize the existing Toolbar component.
const toolbar = Toolbar.init(document.querySelector("#writing-toolbar"));
// Connect public editor commands.
const disconnect = editor.connectToolbar(toolbar);Toolbar connection
The demo uses Rodeo-style crazy-button controls: size="small", shape="round", Material icons, and variant="tonal". Related actions use toolbar-group with toolbar-separator between them. Narrow toolbars scroll horizontally.
connectToolbar() accepts a Toolbar instance or its element and returns a disconnect function. Commands work with native buttons, crazy-button, and native selects. The editor keeps unavailable undo, redo, and movement commands disabled. Connecting a new toolbar replaces the old binding.
| Method / option | Behavior |
|---|---|
data-editor-command="type" | Change the active block to data-editor-value: scene, action, character, dialogue, parenthetical, or transition. |
insert | Insert a block after the selected one; its type comes from data-editor-value. |
up / down / remove | Move or remove the selected block. |
undo / redo | Restore document history, including formatting and order. |
// Use the same methods with any custom controls.
editor.setBlockType("character");
// Add dialogue after the active block.
const id = editor.insertBlock("dialogue", "We made it before the light.");
// Move it to a specific zero-based document position.
editor.moveBlock(id, 2);
// Display pages at 100% without changing screenplay pagination.
editor.setZoom(1);
// Restore responsive parent-width sizing.
editor.setZoom("fit-parent");
// Read the current number of A4 screenplay pages.
const pages = editor.getPageCount();
// Disconnect an external toolbar when no longer needed.
disconnect();Document & export
Documents contain a title and ordered blocks with stable IDs, a semantic type, and literal text. Getters return detached copies. Duplicate or missing IDs are replaced during loading. Invalid imports leave the current document intact. JSON preserves block types and order; text export is readable plain text, not a Fountain or PDF serializer.
// Save through your application’s persistence layer.
const json = editor.toJSON();
// Restore a previously saved document.
editor.fromJSON(json);
// Read a plain-text version.
const text = editor.toText();{"title":"The last light","blocks":[]}Methods & options
| Method / option | Behavior |
|---|---|
getDocument() / setDocument(document) | Read a detached document or replace it as an undoable operation. |
getActiveBlock() | Read a detached copy of the focused block. |
insertBlock(type, text?, afterId?) | Insert and focus a block; returns its ID. |
updateBlock(id, { type?, text? }) | Update supported fields without changing the stable ID. |
setBlockType(type) | Format the active block and retain its text selection. |
moveBlock(id, index) / removeBlock(id?) | Reorder by zero-based index or remove a block. An empty document retains one writable action block. |
focus(id?, start?, end?) | Focus a block and optionally select a text range. |
undo() / redo() / canExecute(command) | Inspect and use the bounded, 100-step document history. |
execute(command, value?) | Invoke the same commands as a connected toolbar. |
getZoom() / setZoom("fit-parent" | number) | Read or change preview size. Numeric scales range from 0.25 to 2; 1 means 100%. Defaults to "fit-parent". Available in read-only mode and independent of document history. |
getPageCount() | Read the number of physical A4 sheets. Page breaks preserve semantic blocks and their source text, including long blocks continued across several pages. |
setReadOnly(boolean) | Disable user edits and mutation commands; explicit document loads remain available. |
destroy() | Disconnect listeners and the toolbar, stop resize observation, and restore the original host markup. The Toolbar instance remains application-owned. |
document / label / readOnly / zoom / onChange | Initialization options. onChange(document) receives a detached document. |
--screenplay-editor-height / --screenplay-page-shadow | Customize the scrollable canvas height (default 640px) and the soft shadow around each page. |
--screenplay-paper-background / --screenplay-paper-color / --screenplay-paper-muted | Customize the paper and ink colors. Page size, margins, and 12-point Courier stay fixed so resizing the preview does not change pagination. |
screenplayzoomchange bubbles with detail.zoom. screenplaylayoutchange bubbles with detail.pages when pagination changes. screenplaychange bubbles with detail.document. screenplayselectionchange bubbles with detail.block. In this demo, try the public window.screenplayEditor instance in your console.