` attribute is passed through too (except `children`, `style`,
and `className`, which the component owns).
When both `width` and `height` are set, the placeholder renders the dimensions
under the hint so you can eyeball aspect ratios while drafting.
## Sizing [#sizing]
Pick one of two modes:
* **Fixed box** — pass `width` and `height` when the layout has a hard image
slot (a hero card, a logo lockup at a known size).
* **Fill the parent** — omit both when the placeholder sits inside a flex or
grid cell that already controls its size. The component defaults to
`width: 100%; height: 100%`.
```tsx
// Fixed
// Fill a flex/grid cell
```
The `hint` should describe the *content* the user has to supply
("Q3 revenue chart"), not its layout role ("hero image").
## Replacement workflow [#replacement-workflow]
The inspector's **Replace…** action is wired to a Vite-plugin op called
`replace-placeholder-with-image`. Given an asset path, it:
1. Confirms the targeted JSX is an `
`.
2. Adds `import from './assets/'` if the asset isn't already
imported.
3. Rewrites the element to:
```tsx
}
alt=""
style={{ width, height, objectFit: 'cover', objectPosition: '50% 50%' }}
/>
```
The asset path must start with `./assets/` (slide-local) or `@assets/`
(global). If you skip the inspector and want to swap the placeholder by
hand, the snippet above is exactly what to write.
## Cropping [#cropping]
After replacement the image is a regular `
` with `objectFit: 'cover'`,
which means the asset's aspect ratio rarely matches the slot exactly. To
reframe the visible region:
* **Double-click** the image while the inspector is active, or
* Click it once and choose **Crop** in the property panel.
The crop dialog writes `objectFit` (Fill = `cover`, Fit = `contain`) and
stores the crop rectangle as an `objectViewBox: inset(…)` value on the same
inline `style` block — no asset is mutated, so you can reopen the dialog
later and adjust.
## When to use it [#when-to-use-it]
Use a placeholder only when a *specific concrete* image is required by the
deck's topic — a product screenshot, a team photo, a customer logo. Don't use
it for decoration or generic stock-photo filler; if a typographic or
iconographic solution would do, prefer that.
# Slide module exports (/docs/reference/slide-meta)
```tsx title="slides//index.tsx"
import type { Page, SlideMeta } from '@open-slide/core';
const Cover: Page = () => {/* ... */}
;
export const meta: SlideMeta = {
title: 'Cover',
theme: 'corporate',
};
export const notes = ['Open with the analyst quote.'];
export default [Cover] satisfies Page[];
```
## Default export [#default-export]
`Page[]` — the array of components, in display order. Each component fills
its container; the runtime renders one at a time onto the 1920×1080 canvas.
```ts
type Page = ComponentType & {
transition?: SlideTransition;
};
```
## `meta` (optional) [#meta-optional]
## `notes` (optional) [#notes-optional]
Plain-text strings (line breaks preserved), index-aligned with the page
array. Surfaced in the presenter view — see
[Present mode](/docs/core-feature/present-mode).
```ts
export const notes: (string | undefined)[] = [
'Open with a smile.',
undefined,
'Pause for questions.',
];
```
## `transition` (optional) [#transition-optional]
A module-level `SlideTransition` becomes the default animation between every
page in the deck. Per-page overrides are assigned on the `Page` component
itself. See [SlideTransition](/docs/reference/slide-transitions) for the full
schema.
```ts
import type { SlideTransition } from '@open-slide/core';
export const transition: SlideTransition = {
duration: 200,
exit: { /* … */ },
enter: { /* … */ },
};
```
## Imports from `@open-slide/core` [#imports-from-open-slidecore]
```ts
import {
CANVAS_WIDTH, // 1920
CANVAS_HEIGHT, // 1080
ImagePlaceholder, // sized empty placeholder — see /docs/reference/image-placeholder
MorphElement, // match an element across pages — see /docs/primitive/morph-element
} from '@open-slide/core';
import type {
MorphElementProps,
Page,
SlideMeta,
SlideModule,
SlideTransition, // per-page / module-level animations — see /docs/reference/slide-transitions
MorphTransition,
TransitionPhase,
ImagePlaceholderProps,
} from '@open-slide/core';
```
# SlideTransition (/docs/reference/slide-transitions)
The framework can run an enter/exit animation between every page change. There
is **no default** — pages snap unless you declare a `SlideTransition`. For
where to declare one and how precedence works, see
[Transition](/docs/primitive/transition); this page is the schema plus a
reusable set of examples.
`prefers-reduced-motion: reduce` is honored automatically — you don't write a
fallback.
## Schema [#schema]
### `SlideTransition` [#slidetransition]
### `TransitionPhase` [#transitionphase]
### `MorphTransition` [#morphtransition]
## Background [#background]
The outgoing and incoming pages are stacked in one container painted with the
deck background (`--osd-bg`), incoming on top. Every page paints its own
background, so while the outgoing page stays opaque the incoming page fades in
over it and the container colour is never visible.
An exit that fades to `opacity: 0` would expose the container before the enter
has covered it — a dip to black on any page whose background differs from
`--osd-bg`. The runtime therefore drops `opacity` from `exit` keyframes;
`transform` and `filter` still apply. Write the fade on `enter` and keep the
exit as a hold:
```ts
export const transition: SlideTransition = {
duration: 260,
exit: { keyframes: [{ opacity: 1 }, { opacity: 1 }] },
enter: { keyframes: [{ opacity: 0 }, { opacity: 1 }] },
};
```
Set `throughBackground: true` when the dip is the point — a section break that
goes to black between two pages that share the deck background. Exit keyframes
then run exactly as written.
```ts
const breath: SlideTransition = {
duration: 460,
throughBackground: true,
exit: { duration: 180, keyframes: [{ opacity: 1 }, { opacity: 0 }] },
enter: { duration: 240, delay: 300, keyframes: [{ opacity: 0 }, { opacity: 1 }] },
};
```
## Direction hook [#direction-hook]
The framework writes two values on the transition wrapper so a single keyframe
can mirror itself on backward navigation:
| Surface | Forward | Backward |
| -------------- | --------- | ---------- |
| `--osd-dir` | `1` | `-1` |
| `data-osd-dir` | `forward` | `backward` |
Use `--osd-dir` inside `calc()`:
```tsx
{ transform: 'translateX(calc(var(--osd-dir, 1) * 8px))' },
{ transform: 'translateX(0)' },
```
Most decks shouldn't mirror on backward navigation — reach for the direction
hook only when the motion has a literal direction (a horizontal advance, a
chapter sweep).
## Morph [#morph]
`morph` enables [MorphElement](/docs/primitive/morph-element) matching: wrap
the same visual object on two pages with the same `id` and the runtime
animates it across the cut instead of fading it. Marked elements that exist on
only one of the two pages fade in or out automatically.
Use the object form when the morph needs its own timing:
```ts
export const transition: SlideTransition = {
duration: 900,
enter: { keyframes: [{ opacity: 0 }, { opacity: 1 }] },
exit: { keyframes: [{ opacity: 1 }, { opacity: 1 }] },
morph: {
duration: 900,
easing: 'cubic-bezier(0.22, 1, 0.36, 1)',
},
};
```
# /apply-comments (/docs/skills/apply-comments)
`/apply-comments` closes the visual feedback loop. You drop comments through
the inspector; the skill resolves them into code edits.
## What it does [#what-it-does]
1. Scans the workspace for `@slide-comment` markers.
2. For each marker, reads the surrounding code and the comment text.
3. Edits the file to satisfy the comment.
4. Removes the marker.
```text
› /apply-comments
✓ 3 markers · slides/q2-launch/index.tsx
✓ 1 marker · slides/intro/index.tsx
```
## Marker semantics [#marker-semantics]
A `@slide-comment` marker is a normal JSX/JS comment that the inspector
inserts next to the element you commented on. It's invisible at runtime —
the rendered output never changes. The marker carries:
* The user's note (the message you typed).
* A pointer to the element (so an agent reading the file knows what *this*
meant).
## When to use it [#when-to-use-it]
After a batch of inspector comments — typically right before saving the
deck for the day or right before a rehearsal. Refreshing the dev server
after `/apply-comments` shows the new state with the markers cleared.
If a comment is ambiguous, the agent applies the smallest reasonable
interpretation and notes the assumption in its summary. Comments it can't
resolve at all are left in place and reported as skipped — refine the
wording and re-run.
# /create-slide (/docs/skills/create-slide)
`/create-slide` is the entry-point skill for any new deck.
## What it does [#what-it-does]
1. **Theme** — if `themes/` contains any themes, asks you to pick one (or
none). A picked theme settles the visual direction. See
[Themes](/docs/core-feature/themes).
2. **Scoping** — asks four questions: aesthetic direction (skipped when a
theme is picked), page count, text density, motion vs. static.
3. **ID & structure** — picks a kebab-case id, plans the page list.
4. **Authoring** — writes `slides//index.tsx` page by page, deferring
to [`/slide-authoring`](/docs/skills/slide-authoring) for the *how*.
## When to use it [#when-to-use-it]
For any new deck. Even if you plan to hand-author, kick off with
`/create-slide` to seed the folder, the page array, and the meta block.
```text
/create-slide for "Q2 launch — 3 chapters, mixed text density, subtle motion, dark aesthetic"
```
## What you get [#what-you-get]
A working `slides//index.tsx` with one component per page, a `meta`
export, and assets imported from `slides//assets/`. Drop into the dev
server and iterate from there.
# /create-theme (/docs/skills/create-theme)
`/create-theme` codifies a visual recipe into a reusable theme file. Run it
once your decks start sharing a visual language.
## What it does [#what-it-does]
Writes a **theme bundle** under `themes/` — two paired files that share a
stem, always produced together:
* `themes/.md` — palette, type stack, layout vocabulary, fixed
components, motion, and voice notes that `/create-slide` reads on its
next run.
* `themes/.demo.tsx` — a runnable 2–3 page mini-slide that the dev UI's
**Themes** panel renders as the theme's live preview.
See [Themes](/docs/core-feature/themes) for the file format and how a theme
is consumed.
The input can be:
* **An existing deck.** The skill scans `slides//index.tsx`, distils the
recurring tokens, and writes them out.
* **A brief.** Describe the system in chat (*"warm editorial, serif display,
high-contrast palette"*) and the skill bootstraps the bundle from
scratch.
* **Image references.** Pass screenshots or mood-board images and the
skill extracts palette, type, and layout cues from them.
Once the bundle exists, `/create-slide` offers the theme on its next run —
see [Themes](/docs/core-feature/themes#using-a-theme).
# /current-slide (/docs/skills/current-slide)
`/current-slide` is what makes deictic prompts work. *"This page"*, *"this
heading"*, *"the slide I'm on"* — the skill teaches the agent how to
resolve them to a concrete slide id, page index, and (when set) the JSX
element you've picked in the [inspector](/docs/core-feature/inspector).
## How it works [#how-it-works]
The dev server writes the user's current view to
`node_modules/.open-slide/current.json` on every navigation and selection
change:
* `slideId` / `pageIndex` — the deck folder name and the active page.
* `pagePath` — the source file of that page.
* `selection` — line/column, tag name, and a text snippet of the JSX node
picked in the inspector (`null` when nothing is picked).
The skill reads that file before asking *which slide?*
## When it fires [#when-it-fires]
You don't usually invoke it directly. `/slide-authoring` cross-references
it, so any "fix this" prompt routes through it automatically.
It collapses the most common round-trip in agent-driven editing:
```text
You: make this title smaller
Agent: which title?
You: the one on slide 3
```
With `/current-slide`, the agent already knows. See
[`/apply-comments`](/docs/skills/apply-comments) for how this folds into the
broader inspector loop.
# Overview (/docs/skills/overview)
open-slide treats your coding agent as a first-class user: every workspace
ships with agent rules and a set of skills the agent invokes by name.
## What ships in the workspace [#what-ships-in-the-workspace]
`npx @open-slide/cli init` generates an `AGENTS.md` (and `CLAUDE.md`) at the
root with the framework's hard rules, plus five skills under
`.agents/skills/` (with `.claude/skills/` symlinks for Claude Code):
* [`/create-slide`](/docs/skills/create-slide)
* [`/slide-authoring`](/docs/skills/slide-authoring)
* [`/apply-comments`](/docs/skills/apply-comments)
* [`/create-theme`](/docs/skills/create-theme)
* [`/current-slide`](/docs/skills/current-slide)
The skills are kept inside the workspace (not the agent's global config) so
they version with the project. `open-slide sync:skills` re-syncs them from
the package if you upgrade.
## The hard rules (`AGENTS.md`) [#the-hard-rules-agentsmd]
* Slides go under `slides//`.
* Entry is `slides//index.tsx`.
* Assets sit in `slides//assets/`.
* Don't touch `package.json`, `open-slide.config.ts`, or other slides.
* Don't add dependencies. Use only React and standard web APIs.
These constraints keep an agent from accidentally installing a UI library,
rewriting global config, or stomping on another deck.
## Bring your own agent [#bring-your-own-agent]
The framework is agent-agnostic. Any tool that can edit React files in a
workspace can author slides — Claude Code, Codex, Cursor, Gemini CLI,
OpenCode, Windsurf, and Zed all work. To author cleanly, the agent needs to:
1. **Read the rules.** `AGENTS.md` (and `CLAUDE.md`) at the workspace root
define the file contract and constraints.
2. **Find the skills.** The five skills above live under `.agents/skills/`
(or wherever that agent looks for skills).
3. **Edit files.** That's it. There is no per-agent SDK.
If your agent of choice doesn't have a skills system, paste the
`slide-authoring` skill into context manually and ask the agent to follow
its rules.
`AGENTS.md` is the canonical rules file — it follows the cross-tool
convention the agents above already look for. `CLAUDE.md` is a symlink to it
(a copy on Windows) so Claude Code picks up the same rules automatically.
# /slide-authoring (/docs/skills/slide-authoring)
`/slide-authoring` is the canonical spec for everything inside
`slides//` — the file contract, the 1920×1080 canvas, type scale,
spacing, palette rules, and per-primitive references. `/create-slide` and
`/apply-comments` defer to it for the *how*.
You don't usually invoke `slide-authoring` directly. Other skills do.
## What it ships [#what-it-ships]
The defaults below are opinionated so an agent doesn't have to reinvent the
look of every deck. Override any of it per-deck, or codify your overrides
in a [theme](/docs/core-feature/themes).
### Type scale [#type-scale]
A 1920×1080 stage pairs well with bigger type than the web is used to:
| Role | Size (px) |
| --------------- | --------- |
| Hero title | 140 – 200 |
| Section heading | 80 – 120 |
| Page heading | 56 – 80 |
| Body | 32 – 44 |
| Caption / label | 22 – 28 |
### Spacing [#spacing]
* Content padding: 100–160 px from the canvas edges.
* Line-height: 1.2 for headings, 1.5–1.7 for body.
* The canvas doesn't scroll — the skill makes the agent budget every page's
vertical space against the 1080 px height before writing JSX, and split
into two pages rather than shrink type to fit.
### Visual direction [#visual-direction]
* **Palette** — 1 background, 1 primary text, 1 accent, 1 muted.
* **Typography** — one display font + one body font; system stack unless
the user specifies.
* **One grid per deck** — a single content padding held across every page.
* **One aesthetic** — minimal, editorial, retro, brutalist, … — chosen up
front and never mixed.
For a recipe-driven design system, write a theme — see
[Themes](/docs/core-feature/themes).