react-web-builder
v0.25.0
Published
React components for building pages
Readme
react-web-builder
A drag & drop page builder for React - with your own components.
Live demo · Documentation · Changelog
What is it?
react-web-builder gives you two components:
<WebBuilder />- the editor. A responsive, zoomable grid where users drag components, edit their properties in a sidebar and design per breakpoint.<View />- the renderer. It takes thepageobject produced by the editor and renders the final page. No editor code, no editing UI.
The key idea: every component in the palette can be your own React component. You describe it once (id, label, icon, editable properties) and the builder generates the whole editing experience for it - inputs, color pickers, rich text editors, image uploads and so on.
Typical use cases: landing page builders inside a SaaS, CMS page editors, email/newsletter layout tools, white-label site builders for your customers.
Features
- 🧩 Bring your own components - any React component becomes a drag & drop block
- 🎛️ Declarative property editor - 20+ property types (text, number, color, richtext, image, url, border, padding, select, list, array, object…)
- 📱 Responsive by design - multiple breakpoints, each with its own grid, columns and layout
- 🖱️ Grid with pan & zoom - powered by
react-grid-panzoom - ✍️ Rich text editing - Slate-based, with colors, links, alignment and font options
- 🧱 Templates - start users from a prepared layout instead of a blank canvas
- 🔤 Custom fonts - declare fonts (e.g. Google Fonts) selectable in page settings
- 🌍 i18n - full translation support via
i18next - 🖼️ Image upload hook - plug in your own storage/CDN
- 💡 Builder hints & custom navbar icons - onboard users and add your own toolbar actions
- 🔒 TypeScript first - types shipped for every entry point
- 📦 ESM + CJS builds, tree-shakable subpath exports
Installation
npm install react-web-builder
# or
yarn add react-web-builder
# or
pnpm add react-web-builderPeer dependencies: react >= 18.2.0 and react-dom >= 18.2.0.
Quick start
1. The editor
import WebBuilder from 'react-web-builder'
export function Editor() {
const onPublish = async (page) => {
// `page` is a plain, serializable object - store it wherever you like
await fetch('/api/pages/home', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(page),
})
}
return <WebBuilder onPublish={onPublish} />
}2. The rendered page
import View from 'react-web-builder/View'
export function Page({ page }) {
return <View page={page} />
}⚠️ Rule of thumb: every prop you pass to
<WebBuilder />that affects rendering -components,container,fonts,transformElementProperty, thedefault*props - must also be passed to<View />. Otherwise the published page will not render the same way it looked in the editor.
3. Persisting pages (full round trip)
import { useEffect, useState } from 'react'
import WebBuilder from 'react-web-builder'
export function EditorWithPersistence() {
const [page, setPage] = useState(null)
const [loading, setLoading] = useState(true)
useEffect(() => {
fetch('/api/pages/home')
.then((res) => res.json())
.then(setPage)
.finally(() => setLoading(false))
}, [])
if (loading) return <p>Loading…</p>
return (
<WebBuilder
page={page ?? undefined} // undefined => start from an empty page
onAutoSave={(next) => save(next, 'draft')}
onSaveAsDraft={(next) => save(next, 'draft')}
onPublish={(next) => save(next, 'published')}
onExit={async () => { window.location.href = '/dashboard' }}
/>
)
}
const save = (page, status) =>
fetch('/api/pages/home', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ page, status }),
})onAutoSave only fires when the user enables Auto save in the configuration menu (top-right corner). onChange fires on every change - use it for undo stacks or live previews, not for network calls.
Built-in components
Out of the box the palette contains:
| Component | id | Notes |
| --- | --- | --- |
| Container | Container | Section wrapper with its own columns, background, padding, background image |
| Text | Box | Rich text block |
| Image | Image | With upload support, link, border, shadow |
| Video | Video | Powered by react-player |
| Custom button | CustomButton | Toggle with defaultButtonAvailable |
| Line | Line | Solid / dashed, configurable thickness and radius |
| Separator | Separator | Vertical spacing |
| iFrame | Iframe | Embed external content |
| Anchor | Anchor | In-page scroll target |
| HTML | HTMLComponent | Raw HTML snippet |
You can extend this list with components - your components are merged into the palette and grouped in the sidebar. Registering a component under an existing id overrides that built-in instead of adding a second entry, which is the easiest way to swap an icon, a label or an implementation.
Container is special: it owns a nested grid, so users can build real sections rather than a flat stack of blocks. Your own components can do the same with isContainer and the useElementContainer hook - see containers.
Your own components
A minimal component
import WebBuilder from 'react-web-builder'
const Pricing = () => <div className="pricing">Our plans</div>
const components = [
{
id: 'pricing',
label: 'Pricing table',
component: Pricing,
},
]
export const Editor = () => <WebBuilder components={components} />With editable properties
Each entry in props becomes a field in the sidebar and is passed to your component as a prop with the same id.
const Hero = ({ title, subtitle, backgroundColor, ctaUrl, rounded }) => (
<section style={{ backgroundColor, borderRadius: rounded ? 16 : 0, padding: 40 }}>
<h1>{title}</h1>
<p>{subtitle}</p>
<a href={ctaUrl?.location} target={ctaUrl?.openInNewTab ? '_blank' : undefined}>
Get started
</a>
</section>
)
const components = [
{
id: 'hero',
label: 'Hero section',
component: Hero,
icon: HeroIcon, // optional React component
defaultWidth: 12, // in grid columns
group: { id: 'marketing', label: 'Marketing', order: 1 },
order: 0,
props: [
{ id: 'title', label: 'Title', type: 'text', defaultValue: 'Ship faster' },
{ id: 'subtitle', label: 'Subtitle', type: 'text', defaultValue: 'Build pages without a developer' },
{ id: 'backgroundColor', label: 'Background', type: 'color', defaultValue: '#f8f8f8' },
{ id: 'rounded', label: 'Rounded corners', type: 'toggle', defaultValue: true },
{ id: 'ctaUrl', label: 'Button link', type: 'url', canOpenInNewTab: true },
],
},
]Property types at a glance
| Type | Value shape | Use for |
| --- | --- | --- |
| text | string | Single-line input |
| number | number | Range slider between min and max (default 0–20) |
| toggle | boolean | On/off switch |
| color | string (hex) | Color picker |
| select | string | Dropdown, needs options[] |
| richtext | TextElement[] | Slate rich text - render with <Box /> |
| fontOptions | FontOptions | Font styling for static text |
| fontFamily | string | Font selector |
| html | HTMLComponentValue | Raw HTML textarea |
| img | { location, upload } | Image picker + upload |
| backgroundImage | BackgroundImage | Background image with sizing |
| url | { location, openInNewTab } | Link field |
| padding | { top, right, bottom, left } | Spacing helper |
| border | { top, right, bottom, left, radius, color } | Border helper |
| boxShadow | string | Shadow picker |
| list | ListOptions | Draggable list |
| array | unknown[] | Repeating group, needs of |
| object | Record<string, unknown> | Nested group of fields, needs of |
| about | - | Info box with optional button (see onAboutClick) |
| hidden | unknown | Value kept in the page, no UI |
Full reference: docs/01-COMPONENTS.md.
Rendering rich text
richtext values are Slate documents. Use the exported Box component to render them faithfully:
import Box from 'react-web-builder/Box'
const Quote = ({ content, author }) => (
<blockquote>
<Box content={content} />
<cite>{author}</cite>
</blockquote>
)
const components = [
{
id: 'quote',
label: 'Quote',
component: Quote,
props: [
{ id: 'content', label: 'Quote', type: 'richtext', colorAvailable: true, hyperlinkAvailable: true },
{ id: 'author', label: 'Author', type: 'text' },
],
},
]Need the raw styles instead of the markup? react-web-builder/useBoxStyle returns a React.CSSProperties object from border / boxShadow values.
Conditional fields
Every property accepts a visibility function. Return a truthy value to show the field.
props: [
{ id: 'showAvatar', label: 'Show avatar', type: 'toggle' },
{
id: 'avatarColor',
label: 'Avatar color',
type: 'color',
visibility: ({ formValues }) => formValues.showAvatar,
},
{
id: 'mobileOnlyNote',
label: 'Mobile note',
type: 'text',
visibility: ({ breakpoint }) => breakpoint.from < 768,
},
]The callback receives { breakpoint, element, formValues, prop }.
Transforming values before render
transformElementProperty lets you rewrite stored values on their way to your components - signing image URLs, prefixing routes, resolving CMS ids:
import WebBuilder, {
WebBuilderComponentProperty,
WebBuilderElementProperty,
} from 'react-web-builder'
const transformElementProperty = (
componentProp: WebBuilderComponentProperty,
elementProp: WebBuilderElementProperty,
): unknown => {
if (componentProp?.type !== 'img') return elementProp.value
return { ...elementProp.value, location: toCdnUrl(elementProp.value.location) }
}
<WebBuilder transformElementProperty={transformElementProperty} />
// remember to pass the same function to <View />Breakpoints
Pages are responsive: each breakpoint has its own grid and its own element layout. Defaults are 360px (mobile) and 1280px (desktop); users can change them in the UI, or you can define them up front.
import WebBuilder, { Page } from 'react-web-builder'
const page = {
breakpoints: [
{
id: 'mobile',
from: 360,
to: null, // null => up to 100% of the available space
cols: 5,
rowHeight: 15,
backgroundColor: '#f8f8f8',
padding: { top: 15, left: 15, right: 15, bottom: 0 },
},
{
id: 'desktop',
from: 1280,
to: null,
cols: 10,
rowHeight: 15,
backgroundColor: '#f8f8f8',
padding: { top: 15, left: 15, right: 15, bottom: 0 },
},
],
} as Page
export const Editor = () => <WebBuilder page={page} />More: docs/03-BREAKPOINTS.md.
Templates
A template is a tree (row / column / component / fixed nodes) attached to a breakpoint. It is applied when the builder opens, so users start from a real layout instead of an empty grid.
const templateDesktop: Tree = {
id: 'root',
type: 'row',
w: 10,
marginTop: 0, marginBottom: 0, marginLeft: 0, marginRight: 0,
children: [
{
id: 'heading',
type: 'component',
w: 8,
marginTop: 0, marginBottom: 0, marginLeft: 2, marginRight: 0,
element: { id: 'el-1', componentName: 'Box', props: [], h: 'auto', x: 0, y: 0, w: 8 },
},
],
}
const page = {
breakpoints: [{ id: 'desktop', from: 1280, to: null, cols: 10, rowHeight: 15,
padding: { top: 15, left: 15, right: 15, bottom: 0 },
template: templateDesktop }],
} as PagePair it with onTemplateRestart to let users reset back to the starting layout. Full example: docs/04-TEMPLATES.md.
Fonts
import WebBuilder, { FontImport } from 'react-web-builder'
const fonts: FontImport[] = [
{
label: 'Inter',
value: 'inter',
fontFamily: "'Inter', sans-serif",
stylesheet: (
<>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" />
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;700&display=swap" rel="stylesheet" />
</>
),
},
]
<WebBuilder fonts={fonts} page={{ fontFamily: 'inter' }} />Pass the same fonts array to <View />. More: docs/05-FONTS.md.
Image upload
onImageUpload receives the file and must resolve to { location, upload }:
import WebBuilder, { OnImageUpload } from 'react-web-builder'
const onImageUpload: OnImageUpload = async (file) => {
const data = new FormData()
data.append('file', file as Blob)
const res = await fetch('/api/upload', { method: 'POST', body: data })
if (!res.ok) throw new Error('Upload failed')
const { location } = await res.json()
return { location, upload: { status: 'ok' } }
}
<WebBuilder onImageUpload={onImageUpload} />Throwing inside the callback surfaces an error toast to the user. More: docs/09-IMAGE-UPLOAD.md.
Translations
English lives in src/locales/en.ts. Pass a translations object with the same shape plus a locale key:
const translations = {
locale: 'pl',
// …the rest of the keys, translated
}
<WebBuilder translations={translations} />More: docs/06-TRANSLATIONS.md.
Builder hints & navbar icons
Onboard users with one-off hints (dismissal is remembered in local storage) and add your own toolbar actions:
<WebBuilder
builderHints={[
{ selector: '[data-id="navbar-button"]', title: 'Save your page here', hasButton: true },
]}
navbarIcons={[
{
id: 'preview',
icon: () => <EyeIcon />,
tooltip: 'Open preview in a new tab',
onClick: ({ page }) => openPreview(page),
},
]}
/>More: hints · navbar icons.
Wrapping the page in your own layout
container wraps everything rendered on the grid - in both the builder and the view - so the editor matches your real site chrome:
const Container = ({ backgroundColor, breakpoint, page, children }) => (
<div className="site-shell" style={{ backgroundColor }}>
<SiteHeader />
{children}
<SiteFooter />
</div>
)
<WebBuilder container={Container} />
<View container={Container} page={page} />API reference
<WebBuilder />
Accepts everything in the shared table below, plus:
| Prop | Type | Description |
| --- | --- | --- |
| builderHints | HelperArrowItem[] | One-off hints pointing at DOM selectors |
| enableDownload | boolean | Show "download page" action |
| enableUpload | boolean | Show "upload page" action |
| navbarIcons | WebBuilderNavbarIcon[] | Extra toolbar buttons |
| pageSettingsExtra | WebBuilderComponentProperty[] | Extra fields in page settings (same shape as component props) |
| presetColors | string[] | Colors shown under "Preset colors" in the picker |
| onAutoSave | (page) => void | Fires on change when Auto save is enabled |
| onChange | (page) => void | Fires on every change |
| onAboutClick | (button) => void | Click on an about property button |
| onBeforeDownloadPage | (page) => { filename?, page? } | Adjust the downloaded file |
| onExit | () => Promise<void> | "Exit" in the Save / Publish menu |
| onImageUpload | (file) => Promise<ImageURL> | See image upload |
| onPublish | (page) => Promise<unknown> | "Publish" |
| onSaveAsDraft | (page) => Promise<unknown> | "Save as draft" |
| onPagePreview | (page) => Promise<unknown> | Preview icon |
| onTemplateRestart | () => Page | Return a fresh page when the user restarts the template |
Shared props (<WebBuilder /> and <View />)
| Prop | Type | Description |
| --- | --- | --- |
| components | WebBuilderComponent[] | Your components |
| container | React.ElementType | Wrapper receiving backgroundColor, breakpoint, page |
| elementContainerDecorator | React.ElementType | Wrapper around each element on the grid |
| defaultBoxContent | TextElement[] | Default content of the Text component |
| defaultButtonAvailable | boolean | Show the custom button component |
| defaultButtonBackgroundColor | string | Default button color (hex) |
| defaultButtonContent | TextElement[] | Default button label |
| defaultButtonHref | string | Default button link |
| defaultImageSrc | string | Placeholder image |
| defaultVideoSrc | string | Placeholder video |
| fonts | FontImport[] | Available fonts |
| translations | Translations | UI translations |
| transformElementProperty | TransformElementProperty | Rewrite property values before render |
| page | Page | Page to load (required on <View />) |
The page object
Plain JSON, safe to store in any database:
| Field | Type | Description |
| --- | --- | --- |
| breakpoints | Breakpoint[] | Grid definition per breakpoint |
| elementsInBreakpoints | internal | Elements placed in each breakpoint |
| elementsExtras | internal | Margins and paddings of elements |
| backgroundColor | string | Page background |
| colors | internal | Colors saved by the user |
| fontFamily | string | Selected font |
More: docs/02-PAGE.md.
Entry points
| Import | Contents |
| --- | --- |
| react-web-builder | WebBuilder (default) + all types |
| react-web-builder/View | View (default) + types |
| react-web-builder/Box | Box - renders richtext values |
| react-web-builder/useBoxStyle | useBoxStyle - border/shadow → CSSProperties |
| react-web-builder/components | Box, Image, Line, Video, IFrame, useElementContainer |
Documentation
- Introduction
- Components
- Page object
- Breakpoints
- Templates
- Fonts
- Translations
- Builder hints
- Navbar icons
- Image upload
- Containers
- The editor
Development
Requires pnpm ^10.
pnpm install
pnpm dev # Storybook playground on http://localhost:6006
pnpm test # unit tests (vitest)
pnpm e2e # end-to-end tests (playwright)
pnpm lint # biome lint --write
pnpm format # biome format --write
pnpm build:lib # type-check + build the library
pnpm build:demo # build the static Storybook demoStories in src/stories double as living examples - Builder.stories.tsx, Templates.stories.tsx, View.stories.tsx and friends mirror the docs above.
Contributing
Issues and pull requests are welcome - open an issue first for larger changes. Releases are managed with changesets: run pnpm changeset and describe your change in the PR.
License
MIT © sasza
