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.
- Premium Dark UI β Webflow/Framer-inspired design with modern glassmorphism effects
- WYSIWYG Editing β Real-time preview with instant visual feedback
- Responsive Viewports β Desktop, Tablet, and Mobile preview with device frames
- Component Tree β Hierarchical layer panel with expand/collapse and drag reordering
- RTL Support β Full support for Arabic and other right-to-left languages
- 13+ Built-in Components β Container, Text, Heading, Button, Image, Link, Divider, Spacer, Input, Textarea, Select, Checkbox, Repeater
- Drag & Drop β Add components from palette to canvas, reorder via drag
- Inline Editing β Double-click text components to edit content directly
- External Nodes β Register custom components from your application
- Node Overrides β Customize built-in components without replacing them
- Visual Style Editor β Typography, spacing, colors, borders, shadows, effects
- Responsive Styles β Per-breakpoint style overrides (mobile, tablet, desktop)
- Flexbox & Grid β Visual layout controls with intuitive UI
- Custom Code β Inject scoped CSS and JavaScript per component
- Color Picker β Full HSL color wheel with hex input
- Data Sources β Connect to REST APIs or use static data
- Visual Binding β Bind component props to data paths with schema browser
- Repeater Component β Render lists with pagination support
- Lazy Loading β Only fetch data that's actually used in the document
- Custom Loaders β Per-source loading skeletons
- Undo/Redo β Full history with keyboard shortcuts
- Keyboard Shortcuts β Delete, Duplicate, Copy/Paste, and more
- JSON Import/Export β Save and load page designs
- i18n Ready β Built-in English and Arabic translations, extensible
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://git@github.com: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://git@github.com:aashahin/sv-page-builder.gitOr add to your package.json:
{
"dependencies": {
"@aashahin/sv-page-builder": "git+ssh://git@github.com:aashahin/sv-page-builder.git"
}
}To pin a specific version/tag/branch:
{
"dependencies": {
"@aashahin/sv-page-builder": "git+ssh://git@github.com: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-builderIf you want to publish to GitHub's private registry:
- Create
.npmrcin 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-builderThis 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
- Node.js 18+ or Bun
- npm, pnpm, yarn, or bun
# 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 devThe 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:packageThis outputs to dist/ with:
- ESM JavaScript files
- TypeScript declarations
- CSS styles
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 toolbar
createPageStore,setPageContext,getPageContextcreateSelectionStore,setSelectionContext,getSelectionContextcreateHistoryStore,setHistoryContext,getHistoryContextcreateViewportStore,setViewportContext,getViewportContextcreateDataSourcesStore,setDataSourcesContext,getDataSourcesContextcreateExternalNodesStore,setExternalNodesContext,getExternalNodesContextcreateOverridesStore,setOverridesContext,getOverridesContextcreateI18nStore,setI18nContext,getI18nContext
ComponentNode,ComponentType,ComponentId,ComponentDefinitionPageDocument,StyleProperties,ResponsiveStylesPropSchema,CustomPropComponentProps,NodeOverrideDataSource,DataBinding,DataSchemaExternalNodeConfig,ExternalNodePropsLocale,Direction,TranslationDictionary,I18nStoreViewportBreakpoint,ViewportConfig,DragData
generateId,generatePageIdfindComponentById,findParentById,cloneTree,cloneDocumentaddComponent,removeComponent,updateComponent,moveComponentscopeCustomCss,executeCustomJsisDataBinding,createBinding,resolveBinding
The 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';- Fork the repository
- Create a feature branch (
git checkout -b feature/amazing-feature) - Commit your changes (
git commit -m 'Add amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request
MIT Β© Abdelrahman Shaheen
Built with β€οΈ using Svelte 5 & TypeScript