A powerful, modern drag-and-drop page builder library for Svelte 5 applications. Built with TypeScript, featuring a premium dark UI inspired by Webflow and Framer.
This is a private package. Use one of the following methods to install in your projects:
Add directly from your Git repository:
# Using SSH (recommended for private repos)
npm install git+ssh://[email protected]:aashahin/sv-page-builder.git
# Or using HTTPS with token
npm install git+https://<TOKEN>@github.com/aashahin/sv-page-builder.git
# With bun
bun add git+ssh://[email protected]:aashahin/sv-page-builder.git
Or add to your package.json:
{
"dependencies": {
"@aashahin/sv-page-builder": "git+ssh://[email protected]:aashahin/sv-page-builder.git"
}
}
To pin a specific version/tag/branch:
{
"dependencies": {
"@aashahin/sv-page-builder": "git+ssh://[email protected]:aashahin/sv-page-builder.git#v1.0.0"
}
}
For local development or monorepo setups:
{
"dependencies": {
"@aashahin/sv-page-builder": "file:../page-builder"
}
}
Or use npm/bun link:
# In the page-builder directory
bun link
# In your other project
bun link @aashahin/sv-page-builder
If you want to publish to GitHub's private registry:
Create .npmrc in your project root:
@aashahin:registry=https://npm.pkg.github.com
//npm.pkg.github.com/:_authToken=${GITHUB_TOKEN}
Build and publish:
# In page-builder
bun run build:package
npm publish --registry=https://npm.pkg.github.com
Install in other projects:
npm install @aashahin/sv-page-builder
This library requires Svelte 5 as a peer dependency:
{
"peerDependencies": {
"svelte": "^5.0.0"
}
}
<script lang="ts">
import { PageBuilder, type PageDocument, type Locale } from '@aashahin/sv-page-builder';
import '@aashahin/sv-page-builder/styles';
let locale: Locale = 'en';
function handleSave(doc: PageDocument) {
console.log('Document saved:', doc);
// Save to your backend
}
</script>
<PageBuilder {locale} onSave={handleSave} />
<script lang="ts">
import { PageBuilder, type PageDocument } from '@aashahin/sv-page-builder';
import '@aashahin/sv-page-builder/styles';
// Load from your backend
const initialDocument: PageDocument = {
id: 'page-1',
title: 'My Page',
root: {
id: 'root',
type: 'container',
props: {},
styles: { padding: '16px' },
children: []
},
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString()
};
</script>
<PageBuilder {initialDocument} onSave={(doc) => saveToBackend(doc)} />
Use PageRenderer to display saved pages without the editor UI:
<script lang="ts">
import { PageRenderer, type PageDocument } from '@aashahin/sv-page-builder';
import '@aashahin/sv-page-builder/styles';
// Load from your backend
let document: PageDocument = /* ... */;
</script>
<PageRenderer {document} />
| Prop | Type | Default | Description |
|---|---|---|---|
locale |
'en' | 'ar' |
'en' |
Current locale for translations |
initialDocument |
PageDocument |
ā | Initial document to load |
onDocumentChange |
(doc: PageDocument) => void |
ā | Callback when document changes |
onSave |
(doc: PageDocument) => void |
ā | Callback when save is triggered (Ctrl+S) |
class |
string |
ā | Additional CSS class |
canvasStyle |
string |
ā | Inline style for canvas (e.g., custom fonts) |
externalNodes |
ExternalNodeConfig[] |
[] |
Custom component definitions |
disableBuiltInNodes |
boolean |
false |
Hide all built-in components |
nodeOverrides |
Record<ComponentType, NodeOverride> |
{} |
Override built-in component definitions |
brandName |
string |
'Page Builder' |
Brand name shown in toolbar |
brandLink |
string |
ā | Link for the brand name |
dataSources |
DataSource[] |
[] |
External data sources for binding |
autoFetchData |
boolean |
false |
Fetch all data sources on mount |
| Component | Description | Accepts Children |
|---|---|---|
| Container | Flexbox/Grid container for grouping elements | ā |
| Repeater | Renders child template for each item in a data source | ā |
| Component | Description | Key Props |
|---|---|---|
| Text | Paragraph text with inline editing | content |
| Heading | H1-H6 headings | content, level |
| Button | Clickable button | content, variant, size |
| Link | Anchor element | content, href, target |
| Image | Image with fit options | src, alt, objectFit |
| Divider | Horizontal rule | thickness, style |
| Spacer | Vertical spacing | height |
| Component | Description | Key Props |
|---|---|---|
| Input | Text input field | placeholder, type, required |
| Textarea | Multi-line input | placeholder, rows |
| Select | Dropdown select | options, placeholder |
| Checkbox | Checkbox input | label, checked |
| Shortcut | Action |
|---|---|
A or / |
Open block picker |
Delete / Backspace |
Delete selected component |
Ctrl + D |
Duplicate component |
Ctrl + C |
Copy component |
Ctrl + V |
Paste component |
Ctrl + Z |
Undo |
Ctrl + Shift + Z |
Redo |
Ctrl + S |
Save |
Escape |
Deselect / Close modal / Exit preview |
Enter |
Start editing text component |
Register your own components to appear in the block picker:
<script lang="ts">
import { PageBuilder, type ExternalNodeConfig } from '@aashahin/sv-page-builder';
import MyVideoPlayer from './MyVideoPlayer.svelte';
const externalNodes: ExternalNodeConfig[] = [
{
type: 'video-player', // Unique type identifier
label: 'Video Player', // Display name
icon: 'Video', // Lucide icon name
category: 'media', // Category in palette
acceptsChildren: false,
defaultProps: {
videoUrl: '',
autoplay: false
},
propsSchema: [
{ key: 'videoUrl', label: 'Video URL', type: 'text' },
{ key: 'autoplay', label: 'Autoplay', type: 'boolean' }
],
component: MyVideoPlayer
}
];
</script>
<PageBuilder {externalNodes} />
External nodes receive node and isEditMode props:
<!-- MyVideoPlayer.svelte -->
<script lang="ts">
import type { ComponentNode } from '@aashahin/sv-page-builder';
interface Props {
node: ComponentNode;
isEditMode?: boolean;
}
let { node, isEditMode = true }: Props = $props();
const videoUrl = $derived((node.props.videoUrl as string) || '');
const autoplay = $derived((node.props.autoplay as boolean) || false);
</script>
{#if isEditMode}
<div class="video-placeholder">
<span>š¬ Video: {videoUrl || 'No URL set'}</span>
</div>
{:else}
<video src={videoUrl} {autoplay} controls></video>
{/if}
const externalNodes: ExternalNodeConfig[] = [
{
type: 'chart',
label: 'Chart',
icon: 'BarChart',
category: 'analytics', // Custom category
categoryLabel: 'Analytics', // Category header text
categoryIcon: 'TrendingUp', // Category header icon
// ...
}
];
| Property | Type | Description |
|---|---|---|
type |
string |
Unique identifier (must not conflict with built-in types) |
label |
string |
Display name in block picker |
icon |
string |
Lucide icon name |
category |
string |
Category: 'layout', 'content', 'media', 'interactive', 'custom', or custom |
categoryLabel |
string? |
Localized label for category header |
categoryIcon |
string? |
Icon for category header |
acceptsChildren |
boolean |
Whether component can contain children |
defaultProps |
object |
Default prop values for new instances |
defaultStyles |
object |
Default style values |
propsSchema |
PropSchema[] |
Property panel schema |
component |
SvelteComponent |
The Svelte component to render |
Customize built-in nodes without replacing them:
<script lang="ts">
import { PageBuilder, type NodeOverride, type ComponentType } from '@aashahin/sv-page-builder';
import MediaGalleryPicker from './MediaGalleryPicker.svelte';
const nodeOverrides: Partial<Record<ComponentType, NodeOverride>> = {
image: {
// Use custom media picker instead of text input for src
propsSchemaOverrides: {
src: {
type: 'custom',
component: MediaGalleryPicker
}
}
},
button: {
// Change default button props
defaultProps: {
content: 'Get Started',
variant: 'primary'
}
}
};
</script>
<PageBuilder {nodeOverrides} />
Create custom editors for any prop using type: 'custom':
<!-- MediaGalleryPicker.svelte -->
<script lang="ts">
import type { CustomPropComponentProps } from '@aashahin/sv-page-builder';
let { value, onChange, label }: CustomPropComponentProps = $props();
async function openGallery() {
const selected = await myMediaGallery.open();
if (selected) {
onChange(selected.url);
}
}
</script>
<div>
<label>{label}</label>
<button onclick={openGallery}>
{value ? 'Change Image' : 'Select Image'}
</button>
{#if value}
<img src={value as string} alt="Preview" />
{/if}
</div>
<script lang="ts">
import { PageBuilder, type DataSource } from '@aashahin/sv-page-builder';
const dataSources: DataSource[] = [
// REST API
{
id: 'products',
name: 'Products',
type: 'rest',
config: {
url: 'https://api.example.com/products',
method: 'GET',
responsePath: 'data', // Extract from response
cacheTtl: 60000 // Cache for 1 minute
},
schema: {
type: 'array',
fields: [
{ key: 'id', label: 'ID', type: 'number' },
{ key: 'name', label: 'Name', type: 'string' },
{ key: 'price', label: 'Price', type: 'number' },
{ key: 'image', label: 'Image', type: 'image' }
]
}
},
// Static data
{
id: 'company',
name: 'Company Info',
type: 'static',
config: {
data: {
name: 'Acme Corp',
tagline: 'Innovation at its finest'
}
},
schema: {
type: 'object',
fields: [
{ key: 'name', label: 'Company Name', type: 'string' },
{ key: 'tagline', label: 'Tagline', type: 'string' }
]
}
}
];
</script>
<PageBuilder {dataSources} />
When editing a component, users can bind props to data sources. Bindings are stored as:
{
"content": {
"$bind": {
"sourceId": "company",
"path": "tagline",
"fallback": "Loading..."
}
}
}
<script lang="ts">
import { PageBuilder, type DataSource, type DataSourceLoadingProps } from '@aashahin/sv-page-builder';
import ProductSkeleton from './ProductSkeleton.svelte';
const dataSources: DataSource[] = [
{
id: 'products',
name: 'Products',
type: 'rest',
config: { url: '/api/products' },
loadingComponent: ProductSkeleton // Custom skeleton
}
];
</script>
<script lang="ts">
import { PageRenderer, type PageDocument, type DataSource } from '@aashahin/sv-page-builder';
let document: PageDocument = /* ... */;
let dataSources: DataSource[] = /* ... */;
</script>
<PageRenderer {document} {dataSources}>
{#snippet loadingSlot({ sources })}
<div class="loading-overlay">
<p>Loading {sources.length} source(s)...</p>
{#each sources as source}
<span>{source.sourceName}</span>
{/each}
</div>
{/snippet}
</PageRenderer>
| Property | Type | Description |
|---|---|---|
id |
string |
Unique identifier |
name |
string |
Display name in UI |
type |
'rest' | 'static' |
Data source type |
config |
RestConfig | StaticConfig |
Type-specific configuration |
schema |
DataSchema? |
Schema for binding editor autocomplete |
description |
string? |
Description shown in UI |
icon |
string? |
Lucide icon name |
loadingComponent |
Component? |
Custom loading skeleton |
| Property | Type | Description |
|---|---|---|
url |
string |
API endpoint URL |
method |
'GET' | 'POST' | ... |
HTTP method |
headers |
Record<string, string>? |
Request headers |
body |
unknown? |
Request body |
responsePath |
string? |
JSONPath to extract data |
cacheTtl |
number? |
Cache TTL in milliseconds |
labelField |
string? |
Field for item labels (item browser) |
thumbnailField |
string? |
Field for thumbnails (item browser) |
The library includes English (en) and Arabic (ar) translations with RTL support.
<PageBuilder locale="ar" />
<script lang="ts">
import { useTranslation, useDirection, useLocale } from '@aashahin/sv-page-builder';
const t = useTranslation();
const direction = useDirection();
const locale = useLocale();
</script>
<p dir={direction}>{t('common.save')}</p>
import { createI18nStore, type TranslationDictionary } from '@aashahin/sv-page-builder';
const fr: TranslationDictionary = {
common: {
save: 'Enregistrer',
cancel: 'Annuler',
// ...
},
// ...
};
src/lib/
āāā builder/
ā āāā stores/ # Svelte stores (page, selection, history, viewport)
ā āāā registry/ # Component definitions
ā āāā data/ # Data sources module
ā āāā utils/ # Utility functions (tree, id, overrides)
ā āāā actions/ # Svelte actions (customCode)
ā āāā external.ts # External nodes API
ā āāā types.ts # TypeScript types
āāā components/
ā āāā PageBuilder.svelte # Main entry component
ā āāā builder/ # Canvas, ComponentRenderer, ComponentWrapper
ā āāā nodes/ # Node implementations (TextNode, ImageNode, etc.)
ā āāā palette/ # Block picker modal
ā āāā properties/ # Properties panel tabs
ā āāā renderer/ # PageRenderer for production
ā āāā toolbar/ # Top toolbar
ā āāā tree/ # Component tree/layers panel
ā āāā ui/ # Shared UI components (Select, ColorPicker)
āāā i18n/
ā āāā translations/ # en.ts, ar.ts
ā āāā index.svelte.ts # i18n store and context
ā āāā types.ts # Translation types
āāā styles/
ā āāā layout.css # Design system (Tailwind v4)
āāā index.ts # Public exports
# Clone the repository
git clone https://github.com/aashahin/sv-page-builder.git
cd sv-page-builder
# Install dependencies
bun install
# Start development server
bun dev
The demo app will be available at http://localhost:5173
| Script | Description |
|---|---|
bun dev |
Start development server |
bun run build |
Build the demo app |
bun run build:package |
Build the library for publishing |
bun run check |
Run type checking |
bun run format |
Format code with Prettier |
bun run lint |
Check formatting |
bun run build:package
This outputs to dist/ with:
The library exports the following:
PageBuilder ā Main editor componentPageRenderer ā Production renderer (no editor UI)Canvas, ComponentRenderer, ComponentWrapper ā Builder internalsComponentPalette, BlocksPalette ā Block picker componentsPropertiesPanel ā Properties panelComponentTree ā Layer treeToolbar ā Top toolbarcreatePageStore, setPageContext, getPageContextcreateSelectionStore, setSelectionContext, getSelectionContextcreateHistoryStore, setHistoryContext, getHistoryContextcreateViewportStore, setViewportContext, getViewportContextcreateDataSourcesStore, setDataSourcesContext, getDataSourcesContextcreateExternalNodesStore, setExternalNodesContext, getExternalNodesContextcreateOverridesStore, setOverridesContext, getOverridesContextcreateI18nStore, setI18nContext, getI18nContextComponentNode, ComponentType, ComponentId, ComponentDefinitionPageDocument, StyleProperties, ResponsiveStylesPropSchema, CustomPropComponentProps, NodeOverrideDataSource, DataBinding, DataSchemaExternalNodeConfig, ExternalNodePropsLocale, Direction, TranslationDictionary, I18nStoreViewportBreakpoint, ViewportConfig, DragDatagenerateId, generatePageIdfindComponentById, findParentById, cloneTree, cloneDocumentaddComponent, removeComponent, updateComponent, moveComponentscopeCustomCss, executeCustomJsisDataBinding, createBinding, resolveBindingThe library uses Tailwind CSS v4 with a custom design system. Import the styles:
import '@aashahin/sv-page-builder/styles';
The design system defines CSS variables for colors, typography, spacing, etc.:
:root {
--color-bg-base: oklch(13.5% 0 0);
--color-bg-elevated: oklch(18% 0 0);
--color-accent-500: oklch(59% 0.18 250);
--color-text-primary: oklch(96% 0 0);
/* ... */
}
Pass custom styles to the canvas:
<PageBuilder canvasStyle="font-family: 'Your Font', sans-serif;" />
This library is written in strict TypeScript. All types are exported for use in your application:
import type {
PageDocument,
ComponentNode,
DataSource,
ExternalNodeConfig
} from '@aashahin/sv-page-builder';
git checkout -b feature/amazing-feature)git commit -m 'Add amazing feature')git push origin feature/amazing-feature)MIT Ā© Abdelrahman Shaheen
Built with ā¤ļø using Svelte 5 & TypeScript