Building Website Builder editor components, theming, and CMS integration using @webiny/website-builder-nextjs. Use this skill when the developer wants to create editor components for the Website Builder, register components with createComponent, define configurable inputs (text, number, boolean, color, select, file, slot, lexical), set up component groups, customize the theme (CSS variables, createTheme, Tailwind bridge, fonts), build Server Components that fetch CMS data, or understand the WB architecture (Admin iframe + Next.js). Also use for anything related to the Website Builder starter kit.
The Webiny Website Builder uses a unique architecture: the Admin editor loads your Next.js app inside an iframe. All component code and styles live in your Next.js project -- Webiny only stores the page structure (which components and what input values). You build editor components with @webiny/website-builder-nextjs, register them via createComponent(), define configurable inputs, and manage theming through CSS custom properties and createTheme().
+----------------------------------------------------------+
| Webiny Admin |
| +----------------------------------------------------+ |
| | Website Builder Editor | |
| | | |
| | sidebar +------------------------------+ | |
| | (inputs) | your Next.js app (iframe) | | |
| | | real components | | |
| | | real styles | | |
| | +------------------------------+ | |
| +----------------------------------------------------+ |
+----------------------------------------------------------+
postMessage (SDK)
+----------------------------------------------------------+
| Your Next.js App (running separately) |
| @webiny/website-builder-nextjs SDK installed |
+----------------------------------------------------------+
Key implications:
git clone https://github.com/webiny/website-builder-nextjs.git my-website
cd my-website
npm install
Ensure @webiny/website-builder-nextjs and @webiny/sdk versions in package.json match your Webiny version (yarn webiny --version in your Webiny project).
# .env
NEXT_PUBLIC_WEBSITE_BUILDER_API_KEY=your_wb_api_key
NEXT_PUBLIC_WEBSITE_BUILDER_API_HOST=https://your-cloudfront-url.cloudfront.net
NEXT_PUBLIC_WEBSITE_BUILDER_ADMIN_HOST=http://localhost:3001
NEXT_PUBLIC_WEBSITE_BUILDER_API_TENANT=root
An editor component has two parts:
inputs prop// src/editorComponents/Banner.tsx
import React from "react";
import { ComponentProps } from "@webiny/website-builder-nextjs";
interface BannerInputs {
headline: string;
ctaLabel: string;
ctaUrl: string;
}
export function Banner({ inputs: { headline, ctaLabel, ctaUrl } }: ComponentProps<BannerInputs>) {
return (
<div className="bg-primary py-12 px-6 text-center text-white">
<h2 className="text-3xl font-bold mb-4">{headline}</h2>
{ctaLabel && ctaUrl && (
<a
href={ctaUrl}
className="inline-block bg-white text-primary font-semibold px-6 py-3 rounded-md"
>
{ctaLabel}
</a>
)}
</div>
);
}
The editorComponents array must be in a "use client" file:
// src/editorComponents/index.tsx
"use client";
import { createComponent, createTextInput } from "@webiny/website-builder-nextjs";
import { Banner } from "./Banner";
export const editorComponents = [
createComponent(Banner, {
name: "Custom/Banner",
label: "Banner",
group: "custom",
inputs: [
createTextInput({
name: "headline",
label: "Headline",
description: "The main headline text.",
defaultValue: "Ready to get started?"
}),
createTextInput({
name: "ctaLabel",
label: "Button Label",
defaultValue: "Get started"
}),
createTextInput({
name: "ctaUrl",
label: "Button URL",
defaultValue: "/"
})
]
})
];
Important: The "use client" directive is required because component registration communicates with the editor via the browser. However, components imported here can still be Server Components if they don't have their own "use client" directive.
Use a namespaced string: "YourNamespace/ComponentName". Component names are stored in page documents -- treat them as stable identifiers; renaming breaks existing pages.
| Factory Function | Use Case |
|---|---|
createTextInput | Single-line text, URLs, labels |
createLongTextInput | Multi-line text |
createNumberInput | Numeric values |
createBooleanInput | Toggle / checkbox |
createColorInput | Color picker |
createDateInput | Date / date-time picker |
createSelectInput | Dropdown with predefined options |
createRadioInput | Radio button group |
createTagsInput | List of tags |
createObjectInput | Nested object (group of sub-inputs) |
createLexicalInput | Rich text (Lexical editor) |
createFileInput | File / media picker |
createSlotInput | Slot for nesting other components |
Each factory accepts: name, label, description, defaultValue, and type-specific options.
Groups organize the editor's component palette:
// src/contentSdk/groups.ts
import { registerComponentGroup, type ComponentManifest } from "@webiny/website-builder-nextjs";
export const registerComponentGroups = () => {
registerComponentGroup({
name: "basic",
label: "Basic",
description: "Components for simple content creation"
});
registerComponentGroup({
name: "custom",
label: "Custom",
description: "Assorted custom components",
filter: (component: ComponentManifest) => !component.group
});
};
The filter option creates a catch-all group for components without an explicit group.
The theme system has three files that work together:
theme.css -- CSS Custom Properties/* src/theme/theme.css */
@import "@webiny/website-builder-nextjs/lexical.css";
:root {
--wb-theme-color-primary: #4632f5;
--wb-theme-color-secondary: #00ccb0;
--wb-theme-color-background: #ffffff;
--wb-theme-color-surface: #f9f9f9;
--wb-theme-color-text-base: #0a0a0a;
--wb-theme-color-text-muted: #6b7280;
--wb-theme-color-border: #e5e7eb;
--wb-theme-font-family: "Inter", sans-serif;
}
.wb-heading-1 {
font-weight: 700;
line-height: 1.2;
font-size: clamp(2rem, 1.5rem + 1.5vw, 3rem);
}
.wb-paragraph-1 {
font-weight: 400;
line-height: 1.6;
font-size: clamp(0.95rem, 0.9rem + 0.25vw, 1rem);
}
theme.ts -- Theme Registration// src/theme/theme.ts
import { createTheme } from "@webiny/website-builder-nextjs";
declare const __THEME_CSS__: string;
export const css = __THEME_CSS__;
export const theme = createTheme({
css,
fonts: ["https://fonts.googleapis.com/css2?family=Inter:[email protected]&display=swap"],
colors: [
{ id: "color-primary", label: "Primary", value: "var(--wb-theme-color-primary)" },
{ id: "color-secondary", label: "Secondary", value: "var(--wb-theme-color-secondary)" },
{ id: "color-background", label: "Background", value: "var(--wb-theme-color-background)" },
{ id: "color-text-base", label: "Text", value: "var(--wb-theme-color-text-base)" }
],
typography: {
headings: [{ id: "heading1", label: "Heading 1", tag: "h1", className: "wb-heading-1" }],
paragraphs: [{ id: "paragraph1", label: "Paragraph 1", tag: "p", className: "wb-paragraph-1" }],
quotes: [{ id: "quote", label: "Quote", tag: "blockquote", className: "wb-blockquote-1" }],
lists: [{ id: "list1", label: "List 1", tag: "ul", className: "wb-unordered-list-1" }]
}
});
colors populates the editor's color pickertypography populates the editor's typography toolbarfonts injects fonts into the editor iframetailwind.css -- Tailwind Bridge/* src/theme/tailwind.css */
@import "tailwindcss";
@theme inline {
--font-sans: InterVariable, sans-serif;
--color-primary: var(--wb-theme-color-primary);
--color-secondary: var(--wb-theme-color-secondary);
--color-text-base: var(--wb-theme-color-text-base);
}
This bridges WB CSS variables to Tailwind tokens, enabling bg-primary, text-primary, etc. in your components.
When switching fonts, update all four places:
| File | What to Update |
|---|---|
src/app/layout.tsx | Font import and config (e.g., import { Geist } from "next/font/google") |
src/theme/tailwind.css | --font-sans token |
src/theme/theme.css | --wb-theme-font-family variable |
src/theme/theme.ts | fonts array URL (must include same weight range as layout.tsx) |
Build editor components that fetch data from the Headless CMS at render time:
// src/editorComponents/ProductListing.tsx
import React from "react";
import { ComponentProps } from "@webiny/website-builder-nextjs";
import { sdk } from "@/lib/webiny";
import type { Product } from "@/lib/types";
import type { CmsEntryData } from "@webiny/sdk";
interface ProductListingInputs {
heading: string;
limit: string;
}
export async function ProductListing({
inputs: { heading, limit }
}: ComponentProps<ProductListingInputs>) {
const parsedLimit = parseInt(limit, 10) || 6;
const result = await sdk.cms.listEntries<Product>({
modelId: "product",
limit: parsedLimit,
sort: ["values.name_ASC"]
});
if (!result.isOk()) {
return <div className="text-red-600">Failed to load products: {result.error.message}</div>;
}
const products: CmsEntryData<Product>[] = result.value.data;
return (
<section className="py-12 px-6">
{heading && <h2 className="text-3xl font-bold text-center mb-8">{heading}</h2>}
<ul className="grid gap-6 sm:grid-cols-2 lg:grid-cols-3 max-w-5xl mx-auto">
{products.map(product => (
<li key={product.id} className="border rounded-lg p-6">
<h3 className="text-xl font-semibold">{product.values.name}</h3>
<p className="text-lg font-bold mt-2">${product.values.price.toFixed(2)}</p>
</li>
))}
</ul>
</section>
);
}
Register it (async Server Components work even though index.tsx is "use client"):
createComponent(ProductListing, {
name: "Custom/ProductListing",
label: "Product Listing",
inputs: [
createTextInput({
name: "heading",
label: "Section Heading",
defaultValue: "Our Products"
}),
createTextInput({ name: "limit", label: "Number of products", defaultValue: "6" })
]
});
To use the Headless CMS SDK, initialize it in src/lib/webiny.ts with a Read API key (see the webiny-sdk skill).
Editor -> saves page document to Webiny API
(document: component name + input values)
Next.js request/build
-> contentSdk.getPage("/slug") -> returns page document
-> DocumentRenderer matches component name to React component
-> Component renders (Server Component may fetch CMS data)
SDK package: @webiny/website-builder-nextjs
Component type: import { ComponentProps } from "@webiny/website-builder-nextjs";
Registration: createComponent(ReactComponent, { name, label, inputs })
Input factories: createTextInput, createNumberInput, createBooleanInput, etc.
Theme: createTheme({ css, fonts, colors, typography })
Groups: registerComponentGroup({ name, label, description })
webiny-sdk -- Using the Headless CMS SDK inside Website Builder componentswebiny-project-structure -- Webiny project setup and extension registration