framejs
framejs runs an ES6 JavaScript module in the browser. A frame is the app: it
lives at a stable, shareable framejs.app/j/<uuid> URL whose content is mutable
— you update the app in place by POSTing its hash params. Your job: turn the
user's request into that browser JavaScript and deliver it the right way for
your environment.
Pick your delivery mode
Choose based on what you can do — not on the request:
- Automation mode — you can run a shell /
node: generate the JavaScript,
then create-or-update the session's framejs.app frame, print its /j/<uuid>
URL, and open it in the browser. This is the default whenever a shell is
available.
- Code-block mode — chat / API only, no shell: respond with only a
single fenced JavaScript code block (open it with a
javascript info string)
and nothing else — no surrounding prose, no files, no URLs. The user pastes it
into the editor at framejs.io.
In both modes the JavaScript you write follows the same rules — read
references/coding-guide.md.
What the request can be
- Create from a prompt — "a bouncing ball animation", "plot y = sin(x)".
- Modify an existing app — the request contains a frame URL. It may be a
https://framejs.app/j/<uuid> or a https://framejs.io/j/<uuid>?token=…
URL (the "Copy frame for AI session" action copies the framejs.io runtime
form), a bare 32-char hex id, or a legacy https://framejs.io/j/<sha256> /
64-char id snapshot. Whenever you see a framejs URL, extract the uuid from
its /j/<uuid> path and target THAT frame (pass the URL straight to --id —
the helper routes the update to the app layer even for a framejs.io URL). You
MUST fetch the existing code first and modify it — see
references/short-url-api.md (§ Modify). If the
URL carries a ?token=<key> query param (from the app's "Copy frame for AI
session" action), pass the whole URL to --id (or the key to --token): the
helper stores it and sends it as the bearer credential so your updates keep
working even after the frame's owner has claimed it. Passing the whole
URL also targets the backend the URL names — so a dev or self-hosted frame
URL (e.g. https://framejs-app.localhost:13747/j/<uuid>?token=<key>) updates
its own stack with no env preconfigured. --app-origin <url> /
--io-origin <url> set the backend explicitly. See
references/short-url-api.md (§ Local / dev
origins).
- Visualize local files — the request references file paths (
./data.csv,
/tmp/results.json). Upload them and pass as inputs — see
references/file-inputs.md.
Automation mode — how to deliver
Generate the code, then use the bundled helper (preferred) or the inline-node
fallback in references/short-url-api.md:
cat << 'JSCODE' | node scripts/framejs.mjs create --state "${SCRATCHPAD}/framejs-frame.json" --title "<short title>" --description "<one-sentence summary>" --screenshot
// your generated browser JS here — $vars, backticks, all special chars are safe inside the heredoc
JSCODE
The helper prints (on stdout) two URLs — the primary
https://framejs.app/j/<uuid> page URL and an immutable snapshot:
https://framejs.io/j/<sha256> URL. It also prints (on stderr) a share-link
lifecycle notice you MUST relay to the user:
- the
/j/<uuid> page is editable and live-updating, but an anonymous
(unclaimed) frame is temporary and will expire — tell the user to open it
and claim it (free account) to keep it permanently;
- the
/j/<sha256> snapshot is an immutable copy of the current app
that expires ~30 days after it is last opened — a good stable share/backup
link, but it never reflects later edits.
Add --module <url> for classic scripts and --input name=value for inputs.
State the app saves with setJson needs no flag — it whitelists itself.
One frame per session, updated in place. Always pass
--state "<path in your scratchpad>/framejs-frame.json" (any writable file
path). The first create mints the frame; re-run create with the same
--state to UPDATE the same frame — the /j/<uuid> URL stays constant and
any open framejs.app page updates live, so give the URL to the user once. Only
when the user wants a separate app in the same session, add --new to start
a fresh frame (subsequent updates then target that new one). To update one
specific frame regardless of state, pass --id <uuid>. On such an in-place
update the helper carries the frame's existing Open Graph data forward
automatically when you pass no --og/--title/--description/--tag, so a
bare re-run never drops the title/description/tags (and the retained og.image
skips a redundant re-screenshot). Pass --title/--description/--tag again
only to change the preview copy.
The browser opens automatically only the first time a frame is minted
(--no-open to skip even that). A later update to the same frame — same
--state, or an explicit --id — does NOT reopen the browser: the page already
open reaches it live through the same-frame subscription, so opening again would
just spawn a redundant new tab.
If a local dev checkout is present, the helper auto-loads its .env and targets
the dev stacks (FRAMEJS_APP_ORIGIN / FRAMEJS_IO_ORIGIN) instead of
production — no action needed on your part.
If the helper prints an out of date update notice (on stderr, at most hourly),
relay it to the user verbatim once — their installed skill is behind the latest
and the notice tells them the one command to update it.
Always pass --screenshot: the helper renders the finished app and stores the
capture as the og:image preview. It is self-guarding — it captures ONLY when
the app has no og.image yet, so it never overwrites an image a previous run
(or the user) already set, and it silently falls back to the image-less URL if
no renderer is available. Capture prefers Playwright when it can be imported
(true network-idle waiting — best for apps that fetch inputs), and otherwise
uses system headless Chrome. Playwright is optional: install it
(npm i -g playwright && npx playwright install chromium) for the more reliable
path, or point $FRAMEJS_PLAYWRIGHT at a dir whose node_modules has it. Tune
with --screenshot-wait <ms> (default 6000 — raise it for apps that load
slowly) and --screenshot-size <w,h> (default 1200,630). Override the Chrome
binary with $CHROME_PATH.
scripts/framejs.mjs is resolved relative to this skill's directory, not
your current working directory — run it from the skill folder, or use its
absolute path (Claude Code exposes that directory as ${CLAUDE_SKILL_DIR}, so
${CLAUDE_SKILL_DIR}/scripts/framejs.mjs always works). If you cannot locate or
run the bundled helper, use the inline-node fallback in
references/short-url-api.md — it needs no script
file.
Always include Open Graph preview tags so the link unfurls nicely when shared —
see the OG rules in references/short-url-api.md:
- New app: derive fresh copy with
--title / --description, add 3–6 topic
words with repeatable --tag (stored as the og.tags string array, rendered
as article:tag meta tags), and pass --screenshot to capture the preview
image.
- Modifying an existing app: the fetched app already carries
og (the
fetch command returns it). Do NOT recalculate it — pass the fetched object
straight back through with --og '<the fetched og JSON>', which preserves
every field (including image and tags). Update the SAME frame with
--id <uuid> (or the same --state). Only set new --title/--description
if the user explicitly asked to change the preview copy. You can still pass
--screenshot: if the fetched og already has an image it is left
untouched; if it has none, a fresh capture is added.
Absolute rules (both modes)
- Browser JavaScript only — it runs in an iframe, NOT Node.js.
- MUST use ES6 module syntax:
export function onInputs(inputs) {}.
- If the app generates, hard-codes, or fetches its own data: put that in a
separate function that only returns the data, do all data-driven rendering
inside
onInputs (interaction/animation/resize rendering stays in its own
handlers), and make the LAST line of the module seed it —
onInputs({ "data.json": generateData() }) — so external inputs can later
replace the generated data with no other code change. See
references/coding-guide.md.
- NEVER create HTML files. NEVER write local
.js files. NEVER use your own
visualization/rendering/widget tools to render the result.
- NEVER modify
root.style.position, root.style.height, or
root.style.width.
- NEVER place clickable or important UI in the top-right corner — the
runtime overlays its Edit button there (80 × 30 px at 10px top/right on
desktop, 120 × 52 px at 8px top / 12px right on mobile). Keep the top-right
140 × 64 px free of buttons, menus, toolbars and legends. See
references/coding-guide.md.
- Saving state in the URL: use the globals
getJson(key) and
setJson(key, value) — always available, nothing to import (saveJson is
an alias of setJson, kept for older frames). NEVER hand-rolled hash parsing:
no regex or split on location.hash, no
URLSearchParams(location.hash.slice(1)), no hand-built #?key=value, and no
localStorage. setJson whitelists the key itself, so nothing else is needed
to make the state survive save / shorten / copy. Writing state does not re-run
the app, so a control can write on every input event. See
references/coding-guide.md.
- IMPORTANT: the visualization MUST look good on mobile and adapt to that
screen size — use responsive sizing (read
root's dimensions / listen for
resize), avoid fixed pixel widths that overflow, keep text and touch targets
legible on small screens.
- In automation mode, NEVER output a code block for the user to copy and NEVER
hand-build a long hash URL as the deliverable — always POST through the frame
API (the helper's
create). Give the user the /j/<uuid> page URL.
- In code-block mode, output ONLY the single fenced JavaScript code block —
nothing else.
References
- references/coding-guide.md — globals, exports,
patterns, CDN libraries, common mistakes.
- references/short-url-api.md — create/update a
frame (
POST /j/<uuid>.json), one-frame-per-session state, Open Graph tags,
the definition hash-param whitelist, API tokens (keep updating after a frame
is claimed), inline fallbacks.
- references/file-inputs.md — upload local files
and wire them in as inputs.
scripts/framejs.mjs — Node helper: create (stdin JS → framejs.app frame),
fetch <id>, upload <path>. Origins: FRAMEJS_APP_ORIGIN /
FRAMEJS_IO_ORIGIN (auto-loaded from a nearby .env in local dev), or
per-run via --app-origin/--io-origin or the origin of a full frame URL
passed to --id/fetch.
1---2name: framejs3description: Create, modify, and share interactive browser apps and visualizations from JavaScript via framejs.io. The app is encoded into a shareable URL that runs instantly — no build, server, or account. Use when the user wants a chart, graph, plot, dashboard, animation, simulation, creative coding sketch, data visualization, or small interactive web tool; when they reference a framejs.io short URL (/j/<sha256>) to modify; or when they want to visualize a local data file (CSV, JSON, image, etc.).4license: MIT5---67# framejs89framejs runs an ES6 JavaScript module in the browser. A **frame** is the app: it10lives at a stable, shareable `framejs.app/j/<uuid>` URL whose content is mutable11— you update the app in place by POSTing its hash params. Your job: turn the12user's request into that browser JavaScript and deliver it the right way for13your environment.1415## Pick your delivery mode1617Choose based on what you can do — not on the request:1819- **Automation mode — you can run a shell / `node`:** generate the JavaScript,20 then create-or-update the session's framejs.app frame, **print its `/j/<uuid>`21 URL**, and open it in the browser. This is the default whenever a shell is22 available.23- **Code-block mode — chat / API only, no shell:** respond with **only** a24 single fenced JavaScript code block (open it with a `javascript` info string)25 and nothing else — no surrounding prose, no files, no URLs. The user pastes it26 into the editor at framejs.io.2728In both modes the JavaScript you write follows the same rules — read29[references/coding-guide.md](references/coding-guide.md).3031## What the request can be32331. **Create from a prompt** — "a bouncing ball animation", "plot y = sin(x)".342. **Modify an existing app** — the request contains a frame URL. It may be a35 `https://framejs.app/j/<uuid>` **or** a `https://framejs.io/j/<uuid>?token=…`36 URL (the "Copy frame for AI session" action copies the framejs.io runtime37 form), a bare 32-char hex id, or a legacy `https://framejs.io/j/<sha256>` /38 64-char id snapshot. Whenever you see a framejs URL, extract the uuid from39 its `/j/<uuid>` path and target THAT frame (pass the URL straight to `--id` —40 the helper routes the update to the app layer even for a framejs.io URL). You41 MUST fetch the existing code first and modify it — see42 [references/short-url-api.md](references/short-url-api.md) (§ Modify). If the43 URL carries a `?token=<key>` query param (from the app's "Copy frame for AI44 session" action), pass the whole URL to `--id` (or the key to `--token`): the45 helper stores it and sends it as the bearer credential so your updates keep46 working even after the frame's owner has claimed it. Passing the **whole47 URL** also targets the backend the URL names — so a dev or self-hosted frame48 URL (e.g. `https://framejs-app.localhost:13747/j/<uuid>?token=<key>`) updates49 its own stack with no env preconfigured. `--app-origin <url>` /50 `--io-origin <url>` set the backend explicitly. See51 [references/short-url-api.md](references/short-url-api.md) (§ Local / dev52 origins).533. **Visualize local files** — the request references file paths (`./data.csv`,54 `/tmp/results.json`). Upload them and pass as inputs — see55 [references/file-inputs.md](references/file-inputs.md).5657## Automation mode — how to deliver5859Generate the code, then use the bundled helper (preferred) or the inline-node60fallback in [references/short-url-api.md](references/short-url-api.md):6162```bash63cat << 'JSCODE' | node scripts/framejs.mjs create --state "${SCRATCHPAD}/framejs-frame.json" --title "<short title>" --description "<one-sentence summary>" --screenshot64// your generated browser JS here — $vars, backticks, all special chars are safe inside the heredoc65JSCODE66```6768The helper prints (on stdout) two URLs — the primary69`https://framejs.app/j/<uuid>` page URL and an immutable `snapshot:`70`https://framejs.io/j/<sha256>` URL. It also prints (on stderr) a **share-link71lifecycle notice you MUST relay to the user**:7273- the **`/j/<uuid>` page** is editable and live-updating, but an **anonymous74 (unclaimed) frame is temporary and will expire** — tell the user to open it75 and **claim it** (free account) to keep it permanently;76- the **`/j/<sha256>` snapshot** is an **immutable** copy of the current app77 that **expires ~30 days after it is last opened** — a good stable share/backup78 link, but it never reflects later edits.7980Add `--module <url>` for classic scripts and `--input name=value` for inputs.81State the app saves with `setJson` needs no flag — it whitelists itself.8283**One frame per session, updated in place.** Always pass84`--state "<path in your scratchpad>/framejs-frame.json"` (any writable file85path). The first `create` mints the frame; **re-run `create` with the same86`--state` to UPDATE the same frame** — the `/j/<uuid>` URL stays constant and87any open framejs.app page updates live, so give the URL to the user once. Only88when the user wants a **separate** app in the same session, add `--new` to start89a fresh frame (subsequent updates then target that new one). To update one90specific frame regardless of state, pass `--id <uuid>`. On such an in-place91update the helper **carries the frame's existing Open Graph data forward92automatically** when you pass no `--og`/`--title`/`--description`/`--tag`, so a93bare re-run never drops the title/description/tags (and the retained `og.image`94skips a redundant re-screenshot). Pass `--title`/`--description`/`--tag` again95only to _change_ the preview copy.9697The browser opens automatically only the first time a frame is minted98(`--no-open` to skip even that). A later update to the same frame — same99`--state`, or an explicit `--id` — does NOT reopen the browser: the page already100open reaches it live through the same-frame subscription, so opening again would101just spawn a redundant new tab.102103If a local dev checkout is present, the helper auto-loads its `.env` and targets104the dev stacks (`FRAMEJS_APP_ORIGIN` / `FRAMEJS_IO_ORIGIN`) instead of105production — no action needed on your part.106107If the helper prints an `out of date` update notice (on stderr, at most hourly),108relay it to the user verbatim once — their installed skill is behind the latest109and the notice tells them the one command to update it.110111Always pass `--screenshot`: the helper renders the finished app and stores the112capture as the `og:image` preview. It is self-guarding — it captures ONLY when113the app has no `og.image` yet, so it never overwrites an image a previous run114(or the user) already set, and it silently falls back to the image-less URL if115no renderer is available. Capture prefers **Playwright** when it can be imported116(true network-idle waiting — best for apps that fetch inputs), and otherwise117uses **system headless Chrome**. Playwright is optional: install it118(`npm i -g119playwright && npx playwright install chromium`) for the more reliable120path, or point `$FRAMEJS_PLAYWRIGHT` at a dir whose `node_modules` has it. Tune121with `--screenshot-wait <ms>` (default 6000 — raise it for apps that load122slowly) and `--screenshot-size <w,h>` (default `1200,630`). Override the Chrome123binary with `$CHROME_PATH`.124125`scripts/framejs.mjs` is resolved **relative to this skill's directory**, not126your current working directory — run it from the skill folder, or use its127absolute path (Claude Code exposes that directory as `${CLAUDE_SKILL_DIR}`, so128`${CLAUDE_SKILL_DIR}/scripts/framejs.mjs` always works). If you cannot locate or129run the bundled helper, use the inline-node fallback in130[references/short-url-api.md](references/short-url-api.md) — it needs no script131file.132133Always include Open Graph preview tags so the link unfurls nicely when shared —134see the OG rules in [references/short-url-api.md](references/short-url-api.md):135136- **New app:** derive fresh copy with `--title` / `--description`, add 3–6 topic137 words with repeatable `--tag` (stored as the `og.tags` string array, rendered138 as `article:tag` meta tags), and pass `--screenshot` to capture the preview139 image.140- **Modifying an existing app:** the fetched app already carries `og` (the141 `fetch` command returns it). Do NOT recalculate it — pass the fetched object142 straight back through with `--og '<the fetched og JSON>'`, which preserves143 every field (including `image` and `tags`). Update the SAME frame with144 `--id <uuid>` (or the same `--state`). Only set new `--title`/`--description`145 if the user explicitly asked to change the preview copy. You can still pass146 `--screenshot`: if the fetched `og` already has an `image` it is left147 untouched; if it has none, a fresh capture is added.148149## Absolute rules (both modes)150151- Browser JavaScript only — it runs in an iframe, NOT Node.js.152- MUST use ES6 module syntax: `export function onInputs(inputs) {}`.153- If the app generates, hard-codes, or fetches its own data: put that in a154 separate function that only returns the data, do all **data-driven** rendering155 inside `onInputs` (interaction/animation/resize rendering stays in its own156 handlers), and make the LAST line of the module seed it —157 `onInputs({ "data.json": generateData() })` — so external inputs can later158 replace the generated data with no other code change. See159 [references/coding-guide.md](references/coding-guide.md).160- NEVER create HTML files. NEVER write local `.js` files. NEVER use your own161 visualization/rendering/widget tools to render the result.162- NEVER modify `root.style.position`, `root.style.height`, or163 `root.style.width`.164- **NEVER place clickable or important UI in the top-right corner** — the165 runtime overlays its Edit button there (80 × 30 px at 10px top/right on166 desktop, 120 × 52 px at 8px top / 12px right on mobile). Keep the top-right167 `140 × 64` px free of buttons, menus, toolbars and legends. See168 [references/coding-guide.md](references/coding-guide.md).169- **Saving state in the URL: use the globals `getJson(key)` and170 `setJson(key, value)`** — always available, nothing to import (`saveJson` is171 an alias of `setJson`, kept for older frames). NEVER hand-rolled hash parsing:172 no regex or `split` on `location.hash`, no173 `URLSearchParams(location.hash.slice(1))`, no hand-built `#?key=value`, and no174 `localStorage`. `setJson` whitelists the key itself, so nothing else is needed175 to make the state survive save / shorten / copy. Writing state does not re-run176 the app, so a control can write on every input event. See177 [references/coding-guide.md](references/coding-guide.md).178- **IMPORTANT: the visualization MUST look good on mobile and adapt to that179 screen size** — use responsive sizing (read `root`'s dimensions / listen for180 resize), avoid fixed pixel widths that overflow, keep text and touch targets181 legible on small screens.182- In automation mode, NEVER output a code block for the user to copy and NEVER183 hand-build a long hash URL as the deliverable — always POST through the frame184 API (the helper's `create`). Give the user the `/j/<uuid>` page URL.185- In code-block mode, output ONLY the single fenced JavaScript code block —186 nothing else.187188## References189190- [references/coding-guide.md](references/coding-guide.md) — globals, exports,191 patterns, CDN libraries, common mistakes.192- [references/short-url-api.md](references/short-url-api.md) — create/update a193 frame (`POST /j/<uuid>.json`), one-frame-per-session state, Open Graph tags,194 the `definition` hash-param whitelist, API tokens (keep updating after a frame195 is claimed), inline fallbacks.196- [references/file-inputs.md](references/file-inputs.md) — upload local files197 and wire them in as inputs.198- `scripts/framejs.mjs` — Node helper: `create` (stdin JS → framejs.app frame),199 `fetch <id>`, `upload <path>`. Origins: `FRAMEJS_APP_ORIGIN` /200 `FRAMEJS_IO_ORIGIN` (auto-loaded from a nearby `.env` in local dev), or201 per-run via `--app-origin`/`--io-origin` or the origin of a full frame URL202 passed to `--id`/`fetch`.