| name | svelte-page-builder |
| description | Build, extend, and render pages with the svelte-page-builder package — a Svelte 5 drag-and-drop Studio editor plus an isomorphic (SSR/SSG/CSR) runtime renderer. Use when integrating the page builder, registering custom components or fields, binding components to premade data queries, configuring save/publish, wiring image upload, or rendering saved documents in SvelteKit. |
svelte-page-builder
A Svelte 5 page builder: compose pages from prebuilt or custom components in a drag-and-drop editor, then render the document under SSR, SSG, or CSR. The package is stateless — your app owns persistence, auth, upload transport, and versioning.
Install
npm install svelte-page-builder
Import the stylesheet once in your app: import 'svelte-page-builder/styles.css';
Two entry points:
| Import | Use for | Environment |
|---|
svelte-page-builder | Studio editor, config, types, tree commands | Client |
svelte-page-builder/runtime | Rendering a saved document | SSR / SSG / CSR |
Import the editor only in client code. Import /runtime on the server to render published pages without the editor/DnD bundle.
Quick start
<script lang="ts">
import { PageBuilder, createBuilderConfig, builtInComponents, type SavePayload } from 'svelte-page-builder';
const config = createBuilderConfig({ components: { ...builtInComponents } });
let document = $state({ version: 1 as const, root: [] });
function onsave(p: SavePayload) { /* p.document, p.status, p.warnings */ }
</script>
<PageBuilder {config} value={document} onchange={(d) => (document = d)} {onsave} />
Render the saved document (works server-side):
<script lang="ts">
import { PageRenderer } from 'svelte-page-builder/runtime';
let { config, document } = $props();
</script>
<PageRenderer {config} {document} />
Document schema
interface BuilderDocument { version: 1; root: BuilderNode[]; }
interface BuilderNode {
id: string;
type: string;
props: Record<string, unknown>;
slots?: Record<string, BuilderNode[]>;
}
Draft/published status lives in your storage and in SavePayload — never inside the document JSON.
Detailed guides
- Full API, custom components, custom fields, data queries, save/publish, upload, DnD, spacing, SSR/SSG/CSR: see README.md.
- Sample integrations in this repo:
examples/ssr (SvelteKit SSR/SSG) and examples/csr (client-only SPA).
Prebuilt components
builtInComponents provides: Heading, Text, Button, Image (with upload), Section (tone + background color/image + content slot), Columns (2–3 columns + background). Layout blocks nest recursively — any component, including other layout blocks, can drop into a slot.
Custom components
createBuilderConfig({
components: {
...builtInComponents,
PricingCard: {
label: 'Pricing card',
category: 'Content',
component: PricingCard,
defaultProps: { plan: 'Pro', price: 29 },
fields: { plan: { type: 'text' }, price: { type: 'number' } },
slots: { features: { label: 'Features', accepts: ['Text', 'Button'] } }
}
}
});
Components receive their props plus a builder context. Render named slots with builder.Slot:
<script lang="ts">
import type { BuilderRenderContext } from 'svelte-page-builder';
let { plan, builder }: { plan: string; builder?: BuilderRenderContext } = $props();
const Slot = $derived(builder?.Slot);
</script>
{#if Slot}<Slot name="features" />{/if}
Custom fields
Built-in field types: text, textarea, number, boolean, select, radio, color, image, query. Register your own with a custom: type:
createBuilderConfig({
fields: { 'custom:icon': { component: IconField } },
components: { Badge: { fields: { icon: { type: 'custom:icon' } } } }
});
A custom field component receives { id, name, field, value, readOnly, onchange } and calls onchange(next).
Data queries (real data)
Bind components to premade async data sources. Declare queries in the config and give a component a query-typed field:
createBuilderConfig({
queries: {
products: { label: 'Products', load: async () => fetch('/api/products').then((r) => r.json()) }
},
components: {
ProductList: {
label: 'Product list', component: ProductList,
defaultProps: { source: '' },
fields: { source: { type: 'query', label: 'Data source' } }
}
}
});
The editor shows a dropdown of query keys. At render time, resolve the referenced queries and pass the data to PageRenderer:
import { collectQueryKeys, resolveQueries } from 'svelte-page-builder';
const data = await resolveQueries(config, collectQueryKeys(document, config));
<PageRenderer {config} {document} {data} />
The bound component reads its data from builder.data. Resolve on the server for SSR/SSG.
Save & publish
onsave({ document, status, warnings }) is the single save mechanism.
- Save draft →
status: 'draft'. Publish → status: 'published'.
- Publish is blocked when the document references components missing from the config (
warnings with code: 'unavailable-components'). Override with <PageBuilder allowPublishWithWarnings />.
Validate or scan yourself with validateDocument(document, config) and scanComponents(document, config).
Upload
Thread upload?: (file: File) => Promise<{ url: string; alt?: string }> to PageBuilder; it reaches every image field. Your handler owns transport and returns a URL — the package never persists files.
Layout & spacing
Every block exposes margin, padding, gap in the inspector. These reserved props apply as inline styles in both the editor and the published page.
SSR / SSG / CSR
The runtime render path (PageRenderer, NodeRenderer, BuilderSlot) uses only isomorphic APIs, so published pages render on the server. The editor (PageBuilder, DnD) is client-only and initializes in $effect. In SvelteKit, load + resolve queries in +page.server.ts and render PageRenderer in the page.