Showcase · Setup · Comments · Development
paraframe is a read-only viewer for UI frames, in the style of a design tool's prototype preview. You write each frame as a Svelte component, and paraframe lays them out on a canvas and plays them one at a time. Frames are real components, so anything inside them stays interactive.
Try the showcase at https://paraframe.convex.works.
- Canvas. Every frame in sections on a board you can pan and zoom.
- Player. One frame at a time, scaled to the window; named flows give you starting points.
- Real components in iframes. Each frame renders at its own size, with its own styles, media queries and fixed elements, isolated from paraframe's chrome.
- Frame links. Links, hotspots on any element, and navigation from code.
- Static previews. Optional images at overview scale that switch to live frames as you zoom in.
- Comments. Optional pinned threads on elements inside frames, through visual-comments.
- Theming.
--pf-*custom properties for the chrome.
Requires SvelteKit 3 and Svelte 5. The chrome ships as plain CSS, so your app needs no Tailwind.
In a SvelteKit app (a new one from npx sv create --template minimal --types ts
works), install paraframe and delete the template's root
src/routes/+page.svelte: the canvas takes its place at /.
npm i -D paraframeDefine the prototype once:
// src/lib/prototype.ts
import { resolve } from '$app/paths';
import { definePrototype } from 'paraframe';
export const prototype = definePrototype({
title: 'My product',
size: { width: 1440, height: 900 }, // default frame size
sections: [
{
id: 'onboarding',
title: 'Onboarding',
frames: [{ id: 'welcome', title: 'Welcome', load: () => import('./frames/Welcome.svelte') }]
}
],
flows: [{ id: 'signup', title: 'Sign up', start: 'welcome' }],
paths: {
canvas: () => resolve(''),
play: (frame) => resolve(`play/${frame}`),
frame: (frame) => resolve(`frame/${frame}`)
}
});Each frame is an ordinary Svelte component:
<!-- src/lib/frames/Welcome.svelte -->
<main>
<h1>Welcome</h1>
<p>Your first frame.</p>
</main>Then add the routes. The canvas and player share a layout that loads paraframe's styles; the frame route loads your product's styles instead.
src/routes/
+layout.ts
(viewer)/+layout.svelte
(viewer)/+page.svelte
(viewer)/play/[frame]/+page.ts
(viewer)/play/[frame]/+page.svelte
frame/[frame]/+page.ts
frame/[frame]/+page.svelte
// src/routes/+layout.ts
export const prerender = true;<!-- src/routes/(viewer)/+layout.svelte -->
<script lang="ts">
import 'paraframe/styles.css';
import type { LayoutProps } from './$types';
let { children }: LayoutProps = $props();
</script>
{@render children()}<!-- src/routes/(viewer)/+page.svelte -->
<script lang="ts">
import { Canvas } from 'paraframe';
import { prototype } from '#lib/prototype.js';
</script>
<Canvas {prototype} />// src/routes/(viewer)/play/[frame]/+page.ts
import { frameEntries } from 'paraframe';
import { prototype } from '#lib/prototype.js';
export const entries = () => frameEntries(prototype);<!-- src/routes/(viewer)/play/[frame]/+page.svelte -->
<script lang="ts">
import { page } from '$app/state';
import { Player } from 'paraframe';
import { prototype } from '#lib/prototype.js';
</script>
<Player {prototype} frameId={page.params.frame ?? ''} />// src/routes/frame/[frame]/+page.ts
import { frameEntries, resolveFrame } from 'paraframe';
import { prototype } from '#lib/prototype.js';
import type { PageLoad } from './$types';
export const entries = () => frameEntries(prototype);
export const load: PageLoad = async ({ params }) => ({
resolved: await resolveFrame(prototype, params.frame)
});<!-- src/routes/frame/[frame]/+page.svelte -->
<script lang="ts">
// Import your product's own CSS here, never paraframe/styles.css.
import { FrameHost } from 'paraframe';
import { prototype } from '#lib/prototype.js';
import type { PageProps } from './$types';
let { data }: PageProps = $props();
</script>
<FrameHost {prototype} resolved={data.resolved} />Setup covers frame sizes and props, static previews and theming. To send someone a link, Share it builds the prototype as a static site and deploys it to GitHub Pages.
To see a complete working prototype, clone this repository and run
pnpm install && pnpm showcase.
Inside a frame:
<script lang="ts">
import { FrameLink, useFrameNav } from 'paraframe';
const nav = useFrameNav();
</script>
<FrameLink to="details">Open details</FrameLink>
<!-- Any element, e.g. a table row -->
<tr {@attach nav.hotspot('details')}>…</tr>
<!-- From code -->
<button onclick={() => nav.go('details')}>Save</button>Clicking where there is no link briefly highlights everything that is one.
Buttons, inputs and links never trigger this. If another element handles
clicks inside the frame (a selectable table row, say), add data-pf-interactive to it.
| View | Keys |
|---|---|
| Player | ← → previous and next frame, R restart the flow (while one is playing), C comment mode, Esc close the open comment, then leave comment mode |
| Canvas | Scroll or drag to pan, ⌘/Ctrl + scroll to zoom, ⇧1 fit all, ⇧0 100%, F or ⇧2 center and fit the selection, ← → select previous and next frame, Enter play (when no button or link has focus), C comment mode, Esc close the open comment, then leave comment mode |
On the canvas, hover live frames to inspect tooltips and hover states, and double-click a frame to play it.
The screenshots show an example invoicing prototype; pnpm showcase runs a
smaller one.
Optional. Comments come from a visual-comments server
through your prototype's own server, which also signs viewers in. Install
visual-comments next to paraframe, wrap the viewer layout in <Comments>
from paraframe/comments, and viewers can press C to pin threads on
elements inside frames. Without it, paraframe needs nothing from
visual-comments. See Comments.
Storybook and Histoire are built around stories: each renders a component (or, in Storybook, a page) in isolation in one state, with controls to vary its props. paraframe shows screens: whole pages of your product laid out side by side like a design file and linked into flows you can click through, so you can review a prototype end to end or send it to someone who doesn't read code. Use both if you like; a story is a component in one state, a frame is a screen.





