@pride7/frameseq
v0.35.0
Published
Build editable, AI-friendly presentations with a linear TypeScript DSL.
Maintainers
Readme
FrameSeq
Write slides as an editable TypeScript document—not a tree of presentation markup.
FrameSeq is an AI-friendly presentation framework with a linear authoring model, useful design defaults, chainable styling, and HTML/PDF/PPTX/Typst output. It feels like building an interface, but the source stays as direct and readable as a document.
中文文档 · Try FrameSeq online · Explore the live Gallery · Read the documentation · See what changed
presentation({
title: "A Better Way to Write Slides",
author: "Ada Lovelace",
theme: "minimal-academic",
});
slide().cover();
slide("Why FrameSeq");
text("Presentation code should describe ideas, not boilerplate.").lead();
bullets(
"One linear TypeScript file",
"Semantic layouts with useful defaults",
"Tailwind styling without configuration",
"Interactive HTML, PDF, editable PowerPoint, and Typst",
);
slide("Results").grid(3);
cell(0); metric("12 min", "To first draft");
cell(1); metric("1 file", "To maintain");
cell(2); metric("3 formats", "HTML · PDF · PPTX");
slide("The model");
text`Every command belongs to the current slide until the next $\operatorname{slide}()$ call.`;No imports, wrapper components, nested DOM, or export statement are required in a .slides.ts entry file.
Table of contents
- Live Gallery
- Why FrameSeq
- Quick start
- The document model
- Content
- Layout
- Styling
- Themes and typography
- Visual Studio Code extension
- Preview, build, and export
- AI-friendly layout checks
- Presenter view
- Advanced object API
- Documentation
- Continuous integration and changelog
- Development
Live Gallery
The FrameSeq Gallery contains live, clickable presentations built from this repository: a product tour, a complete AI-oriented research example, and previews of all seven built-in themes. The previews are compiled FrameSeq pages rather than screenshots, so they always track the current runtime and themes.
Why FrameSeq
Easier to edit than raw HTML
HTML is an excellent rendering target, but presentation source written directly as HTML quickly mixes content, nesting, layout, and styling. FrameSeq keeps the author's intent visible:
slide("Architecture").split("40:60");
image("pipeline.png", "Compiler pipeline");
right();
text("Three clear stages").lead();
bullets("Parse the document", "Build the page tree", "Render HTML, PDF, or PPTX");A slide starts with slide(); everything after it belongs to that slide until the next slide(). Layout changes are named operations such as split(), grid(), right(), and cell(). A local visual change stays attached to the object it affects.
This makes presentation code easier to read, rearrange, diff, and revise than a large hierarchy of generic elements.
A strong target language for AI
FrameSeq is deliberately small and regular, which makes it well suited to AI-generated presentations:
- The source expresses presentation intent instead of DOM implementation details.
- Slide boundaries are explicit and require no indentation tree.
- Semantic commands such as
lead(),bullets(),metric(), andsplit()reduce arbitrary design decisions. - Local edits usually change a few lines without rebuilding an HTML structure.
- TypeScript catches misspelled APIs and invalid arguments.
frameseq check --jsongives agents empty-slide, measured overflow, clipping, and readability diagnostics they can correct directly.- Themes and presentation-wide typography let AI change the visual system without rewriting every slide.
- The FrameSeq runtime owns rendering, navigation, responsive scaling, and HTML, PDF, PPTX, and Typst output.
An AI can generate a useful first draft with the semantic API, then a person can edit the same compact source. When a page needs more control, both can progressively add Tailwind utilities, custom themes, or low-level objects without abandoning the document.
Give a coding agent the compact llms.txt contract, then use the AI generation guide for prompting, validation, and correction loops. The Gallery includes a complete AI-oriented research example and its editable source.
Progressive control
Start with defaults:
slide("One idea");
text("A normal slide already has spacing and typography.");
bullets("First point", "Second point");Add a structured layout:
slide("Results").grid(3);
cell(0); metric("42%", "Growth");
cell(1); metric("18K", "Users");
cell(2); metric("99.9%", "Uptime");Style one object:
text("Important")
.style("text-4xl font-bold tracking-tight text-blue-600");Or drop down to the explicit object API for a completely custom composition. FrameSeq does not force every slide into the same abstraction level.
Quick start
Open the editable FrameSeq Playground in StackBlitz to change slides.ts and see the live presentation without installing anything.
Create a presentation project:
npm create frameseq@latest my-talk
cd my-talk
npm install
npm run devThe generated project contains:
my-talk/
├── AGENTS.md
├── CLAUDE.md
├── components/
│ ├── content.ts
│ └── theme.ts
├── slides.ts
├── package.json
└── tsconfig.jsonEdit slides.ts; the browser preview updates as the file changes. Put reusable content functions in components/content.ts and project-wide design settings in components/theme.ts. The entry imports these files directly, without an extra index.ts layer. AGENTS.md gives coding agents the project's FrameSeq syntax, region, formula, and validation rules; CLAUDE.md imports the same instructions so they stay in sync.
To use FrameSeq in an existing TypeScript project:
npm install --save-dev @pride7/frameseq
npx frameseq new my-talk.slides.ts
npx frameseq dev my-talk.slides.tsThe document model
presentation("My Talk");
slide().cover();
text("Build slides like apps").hero();
text("Keep them as easy to edit as documents").subtitle();
text("Your name").author();
slide("Why");
text("Common slides use useful defaults.");
bullets(
"No layout boilerplate",
"KaTeX-powered formulas",
"Browser preview and PDF export",
);
slide("Equation");
math`\int_{-\infty}^{\infty} e^{-x^2}\,dx = \sqrt{\pi}`;presentation() defines presentation-wide metadata and design options. slide() starts a page. Content commands add objects to the current page and return chainable builders.
The FrameSeq compiler injects the authoring commands and exports the presentation automatically. Ordinary TypeScript imports, variables, URLs, and functions remain available when the slides need them.
Content
Text roles
text("Main title").hero();
text("Supporting message").subtitle();
text("SECTION 01").eyebrow();
text("The important idea").lead();
text("Source: Example").caption();
text("A memorable sentence").quote();Text roles provide consistent, theme-aware styling while remaining individually customizable.
Formulas
Use a tagged template for display math:
math`\int_{-\infty}^{\infty} e^{-x^2}\,dx = \sqrt{\pi}`;Use $...$ inside a tagged text template for inline math:
text`Euler's identity is $e^{i\pi} + 1 = 0$.`;The tagged forms preserve backslashes, so String.raw is unnecessary.
Typst for complex typesetting
Keep FrameSeq in control of the presentation structure and use Typst for a complex local fragment. Install the optional build-time compiler:
npm install --save-dev @myriaddreamin/typst-ts-node-compilerThen use a static tagged template:
slide("Optimization");
text("Objective function").lead();
typst`
#set text(size: 22pt)
$ min_theta sum_(i=1)^n
loss(f_theta(x_i), y_i) + lambda norm(theta)^2 $
`
.width(720);For larger fragments, keep Typst in its own file:
typstFile("./figures/architecture.typ")
.width(percent(100));FrameSeq compiles embedded Typst to inline SVG during the Vite build. The browser receives no Typst compiler or WASM, and the fragment participates in normal FrameSeq layout and styling across HTML, PDF, and PPTX output. FrameSeq can also export the whole presentation as editable .typ source; native Typst fragments remain source, LaTeX formulas and basic text use MiTeX, and common LaTeX tables become native Typst tables. See Typst integration and export.
LaTeX tables
Keep KaTeX for ordinary formulas and use the optional Tectonic backend when a slide needs a real LaTeX table. Install it locally:
npm install --save-dev node-tectonicThen place the table like any other FrameSeq object:
slide("Results");
latex`
\begin{tabular}{lrr}
\toprule
Model & Accuracy & Latency \\
\midrule
Baseline & 91.2\% & 18 ms \\
FrameSeq & \textbf{94.6\%} & 12 ms \\
\bottomrule
\end{tabular}
`
.width(820)
.position({ x: 180, y: 210 });latexFile("./tables/results.tex") keeps larger fragments in their own files. FrameSeq compiles static fragments at build time, embeds the generated fonts, caches the result by content, and preserves it across HTML, PDF, and PPTX export. See LaTeX integration for supported packages and restrictions.
Code, lists, images, and reveals
code(`const answer = 42;`, "ts");
bullets("Readable source", "Useful defaults", "Flexible styling");
steps("Introduce the problem", "Reveal the model", "Show the result");
image("diagram.png", "System diagram");steps() reveals items during navigation. PDF and PPTX export include the fully revealed slide.
Layout
FrameSeq uses a single-column layout by default. Add structure only where it communicates something useful.
Split
slide("Architecture").split("40:60");
image("diagram.png", "Compiler diagram");
right();
text("Compiler").lead();
bullets("TypeScript DSL", "HTML renderer", "PDF export");Grid
slide("Comparison").grid(3);
cell(0);
text("Simple").lead();
text("Useful defaults");
cell(1);
text("Flexible").lead();
text("Tailwind or object styles");
cell(2);
text("Portable").lead();
text("HTML and PDF");Region paths
Grouping usually forces source to nest. at(path) addresses a region instead, and creates the containers it names, so composition stays flat — one statement per object, no nesting, no closing call:
slide("Roadmap").grid(2);
at("cell0/now").card();
text("Q3").eyebrow();
bullets("Anchors", "Region paths");
at("cell1/next").card();
text("Q4").eyebrow();
at("cell0/now");
text("Merged into main").caption();The first segment can address a region the layout already owns, so at("cell1") and cell(1) are the same region. Every other segment is created on first use, at any depth. Revisiting a path appends to the same region, which means a page can be written in the order that reads best. Paths reset with the next slide(), and each one doubles as an anchor name, so line().to("stages") can connect to a positioned region.
Local grid section
Keep the slide in its normal top-to-bottom flow and arrange only one group of objects as a grid. Each object passed to gridSection() becomes one cell:
slide("Results");
text("Performance this quarter");
gridSection(
3,
metric("42%", "Growth"),
metric("18K", "Users"),
metric("99.9%", "Uptime"),
).gap(20);
text("All targets were exceeded.");Use card() for a common title-and-copy cell, or group() when one cell needs several independently styled objects:
gridSection(
3,
card("Declarative", "Readable source code"),
card("Portable", "HTML, PDF and PPTX"),
group(
text("AI-friendly").bold(),
text("Easy to generate and revise"),
).card(),
);Unlike slide().grid(3), a local grid does not take over the whole slide body. Content before and after it remains in the ordinary document flow.
When the container reads better than its contents, name it with at() and keep writing normally:
at("panel").card().padding(24);
text("Revenue").bold();
text("$1.2M").size(42);Calling .canvas() on a container creates a local coordinate system for positioned children, and .clip() keeps them inside its bounds.
Objects can also be grouped and restyled by name, which keeps the source free of local variables:
rect("Parse").as("parse");
rect("Build").as("build");
rect("Render").as("render");
group("parse", "build", "render").row().gap(16);
ref("parse").fill("#dbeafe");ref(name) returns the same builder the object was created with, so shapes keep .fill(), connectors keep .arrow(), and containers keep .gap(). Names come from .as() or from an at() path.
Freeform positioning
Switch the slide body to a canvas when an object needs exact coordinates:
slide({ name: "System map" }).canvas();
text("Compiler")
.position({ x: 80, y: 90 })
.width(320)
.size(32)
.bold();
image("diagram.png", "Compiler diagram")
.position({ x: 500, y: 80 })
.width(620);Coordinates are relative to the current canvas region. Numbers are pixels in FrameSeq's fixed presentation coordinate system; the default presentation canvas is 1280 x 720, and FrameSeq maps that finished geometry consistently to interactive HTML, PDF, and PPTX. Prefer structured layouts for most pages, then use canvas() with position() for diagrams and custom compositions.
Shapes and connectors
Create editable diagram nodes directly in the slide source. FrameSeq renders boxes and circles as HTML, while lines and arrowheads remain vector SVG:
slide({ name: "Pipeline" }).canvas();
line({ x1: 320, y1: 180, x2: 520, y2: 180 })
.stroke("#2563eb")
.strokeWidth(4)
.arrow("end");
rect("TypeScript")
.position({ x: 80, y: 125 })
.width(240)
.height(110)
.fill("#dbeafe")
.stroke("#2563eb")
.radius(18);
circle("FrameSeq")
.position({ x: 520, y: 100 })
.width(160)
.fill("#cffafe")
.stroke("#0891b2");Write connectors before nodes when the lines should appear behind them.
Diagrams that reference names instead of numbers survive editing. Name an object with as(), then place and connect the rest relative to it:
slide({ name: "Training loop" }).canvas();
rect("Encoder").as("enc").position({ x: 80, y: 140 }).width(200).height(100);
rect("Decoder").as("dec").rightOf("enc", 140);
circle("Loss").as("loss").rightOf("dec", 140);
line().from("enc").to("dec").arrow("end");
line().from("loss.bottom").to("enc.bottom").arrow("end");
text("shared vocabulary").caption().below("enc", 16);A row or column removes the coordinates altogether: the container carries the only one, its children lay themselves out, and connectors still find them.
slide({ name: "Pipeline" }).canvas();
at("stages").row().gap(80).anchor("center");
rect("Parse").as("parse");
rect("Build").as("build");
rect("Render").as("render");
line().from("parse").to("build").arrow("end");
line().from("build").to("render").arrow("end");Moving the diagram is one edit, and anchor() places the container against the slide, so this page has no coordinates at all. Placement modifiers are rightOf(), leftOf(), above(), below(), centerOn(), alignTop(), alignLeft(), and anchor(). Connectors pick the facing edges automatically, or take an explicit anchor such as "enc.top-right". Moving one box updates every connector attached to it. See Diagrams for the complete model — region paths, automatic layout, names, and connectors — and Shapes and connectors for the primitives, arrow directions, layering, and custom SVG assets.
Other layout tools include center(), fullBleed(), left(), and main().
Styling
Tailwind, built in
Pass Tailwind utilities directly to style(); no Tailwind config is required:
text("Designed in the source")
.style("rounded-2xl bg-slate-900 px-8 py-5 text-3xl font-semibold text-white");Arbitrary utilities such as text-[30px], left-[80px], and bg-[#0f172a] are supported.
Chainable modifiers
text("A precise annotation")
.size(18)
.weight(600)
.color("#475569")
.margin(12, 0);The object overload of style() accepts inline CSS when needed:
text("Custom").style({
letterSpacing: "0.08em",
textTransform: "uppercase",
});Themes and typography
FrameSeq starts with a neutral blank theme. Choose a bundled theme:
presentation({ title: "My Talk", theme: "midnight" });Academic themes can build title bars, metadata footers, and slide numbers from presentation metadata:
presentation({
title: "Diffusion Language Models",
subtitle: "From iterative recovery to continuous state spaces",
author: "Your Name",
institute: "Your Institute",
date: "2026",
theme: "minimal-academic",
});
slide().cover();Override presentation-wide typography without creating a theme:
presentation({
title: "My Talk",
theme: "minimal-academic",
font: {
family: '"Noto Sans SC", sans-serif',
size: 24,
heading: { family: '"Noto Serif SC", serif', weight: 700 },
code: { family: '"JetBrains Mono", monospace', size: 18 },
},
});Or define a reusable design system:
const ocean = defineTheme({
name: "ocean",
colors: {
background: "#effcff",
foreground: "#15343b",
accent: "#007c91",
},
});
presentation({ title: "Ocean Research", theme: ocean });Visual Studio Code extension
FrameSeq includes a companion VS Code extension that keeps slides.ts on the left and a live preview on the right. It also provides a slide outline with source-and-preview synchronization, current/previous/next slide navigation, a current-slide status item, slide insertion, TypeScript snippets, Problems-panel layout diagnostics, and HTML/PDF/PPTX/Typst export commands. It consumes the same local FrameSeq CLI as the terminal workflow, so there is only one rendering and validation implementation.
Synchronization also runs the other way. While frameseq dev is serving, hold Alt over the preview to outline the object under the pointer, and Alt-click it to jump to the command that wrote it: in the VS Code preview the cursor moves in the editor beside it, and in a browser the development server opens the file. A command inside a loop or a helper function produces several objects, so more than one of them can lead back to the same line. Source positions are recorded only while serving; a built presentation has none.
The E control goes one step further and turns on layout editing. An object placed with position({ x, y }) can then be dragged, and one given a width() or a height() can be resized from its corner; releasing the drag rewrites those numbers in slides.ts and the preview reloads from the new source. Only the digits change, so comments and formatting are untouched, and a drag is refused outright if the numbers no longer match the file on disk. Escape leaves the mode.
Ctrl+Z in the preview undoes the last drag. In a browser the development server keeps the text each drag replaced and puts it back verbatim; inside the VS Code preview the drag is applied through the workspace instead of written to disk behind the editor's back, so the editor's own undo covers it. Either way the history is dropped rather than misapplied once the document no longer matches it.
An object in the document flow has no coordinates to rewrite, so dragging it changes its place among its neighbours instead: the preview draws where it would land, and releasing it carries the whole lines its command occupies there, with any comment written above or beside it. This stays inside one run of the document—a left(), a cell(1), a new slide(), or a group(a, b) that collects the objects above it each end a run, since carrying lines across one would change which region an object belongs to rather than only its order.
Editing is deliberately limited to numbers the document states outright. position({ x: cursor, y: 90 }) computes its x, and for (const stage of stages) rect(stage) writes one command that becomes several objects; in both cases no single number stands for what was dragged, so FrameSeq offers no handle rather than guessing. The source stays the only description of the talk—dragging is just another way to type a number into it.
Build the current extension from this repository:
npm run vscode:package
code --install-extension output/vscode/frameseq-vscode.vsixThe extension lives in packages/vscode-frameseq and the complete setup guide is in Visual Studio Code extension.
Preview, build, and export
# Live browser preview
frameseq dev my-talk.slides.ts
# Static interactive website in dist/
frameseq build my-talk.slides.ts
# One self-contained dist/index.html
frameseq build my-talk.slides.ts --single-file
# PDF in output/pdf/
frameseq pdf my-talk.slides.ts
# Editable PowerPoint in output/pptx/
frameseq pptx my-talk.slides.ts
# Pixel-faithful PowerPoint with one image per slide
frameseq pptx my-talk.slides.ts --flatten
# Editable Typst source in output/typst/
frameseq typst my-talk.slides.tsThe default static build uses relative asset paths, so dist/ can be hosted at a domain root or a repository subpath such as GitHub Pages. New projects include an Actions workflow that publishes dist/ on pushes to main or master. The single-file build embeds the framework's scripts, styles, fonts, and favicon into one directly openable HTML file. PPTX export keeps normal text and shapes editable while using high-resolution image fallbacks for math, Typst, and LaTeX fragments; --flatten turns each complete slide into one image for maximum fidelity. Typst export keeps pages, grids, text, code, shapes, and positions editable, uses MiTeX for LaTeX math and basic text, and maps common LaTeX tables to native Typst tables. See Deploy HTML, Export PowerPoint, and Typst integration and export.
Arrow keys, Page Up/Page Down, and Space navigate the interactive presentation.
AI-friendly layout checks
FrameSeq can inspect the final browser geometry and return actionable diagnostics instead of asking an AI agent to guess from source code:
frameseq check my-talk.slides.ts
frameseq check my-talk.slides.ts --json
frameseq check my-talk.slides.ts --strictThe checker reports empty slides, objects outside the canvas, clipped text, and unreadably small type. Each issue includes the slide label, FrameSeq object type and path, relevant measurements, and suggested corrections. Mark a deliberate blank page with slide().allowEmpty(); --json produces stable machine-readable output for coding agents and CI. See AI-friendly layout checks.
Presenter view
Attach private notes to any slide:
slide("Architecture")
.notes("Explain why FrameSeq owns structure while Typst owns complex local layout.");While viewing the presentation, press P or use the P button to open presenter view in a second window. It shows the current slide, next slide, speaker notes, timer, page selector, navigation controls, and a synchronized virtual laser pointer. Press Ctrl+L in presenter view to toggle the laser. The audience and presenter windows stay synchronized in both directions without a server.
For a phone remote on the same Wi-Fi, run:
npm run presentSelect the R control and scan the QR code. The phone starts with large navigation controls, a current-slide preview, and a touch laser surface. Select Presenter view on the phone to see speaker notes, the next-slide preview, timer, and page selector; Simple remote returns to the touch-friendly controller without losing the pairing session. FrameSeq relays navigation, reveal steps, and pointer coordinates through the local presentation server—no account, internet connection, or cloud service is required.
You can also append ?presenter=1 to the audience page's current URL. For example, if the audience page is running on port 5174:
http://localhost:5174/?presenter=1Use the port printed by npm run dev; Vite does not always use 5173 when that port is occupied.
Same-device synchronization uses BroadcastChannel; phone pairing uses a local WebSocket only while npm run present is active. See Presenter view.
Advanced object API
The linear authoring layer is recommended for most slides. The low-level UI layer is available for special compositions:
import { Column, Image, Row, Slides, Text } from "@pride7/frameseq";
const slides = Slides("Custom layouts");
slides.slide("Architecture").custom(
Row(
Image("diagram.png"),
Column(
Text("Compiler").size(32).bold(),
Text("DSL → HTML → PDF").size(20),
).gap(16),
).gap(40),
);
export default slides;This layer includes SlidesRoot, Slide, Row, Column, Stack, Text, Image, Code, Equation, and chainable modifiers.
Documentation
Continuous integration and changelog
Every push and pull request runs the complete release:check suite on GitHub Actions: types, themes, desktop and mobile browser behavior, layout diagnostics, remote control, Gallery, static and single-file HTML, PDF, editable and flattened PPTX, and installation from packed npm tarballs. A separate least-privilege workflow builds the live Gallery and deploys it to GitHub Pages only from main.
User-visible changes are recorded in CHANGELOG.md. Add new work under Unreleased, then move it into a dated semantic-version section when publishing.
Development
Before publishing a FrameSeq release locally:
npm run release:checkThe release check builds the library, demo, and Gallery, runs browser tests at desktop and mobile viewport sizes, packs and installs the npm tarballs in a clean project, type-checks the generated document, builds static HTML, and exports PDF and PPTX files.
Maintainers can instead publish both npm packages from a matching version tag through GitHub OIDC. See Release FrameSeq for the one-time Trusted Publisher configuration and tag workflow.
