Admin extensions customize the Webiny Admin application. There are three main categories: white-labeling (logos, titles, theme colors), custom data list columns (adding columns to content entry tables), and custom page-type forms (custom form fields for Website Builder page types). All are React components registered via <Admin.Extension> in webiny.config.tsx.
YOU MUST include the full file path with the .tsx extension in every src prop. For example, use src={"/extensions/MyAdminExtension.tsx"}, NOT src={"/extensions/MyAdminExtension"}. Omitting the file extension will cause a build failure.
// extensions/AdminBranding/AdminTheme.tsx
import React from "react";
import { AdminConfig } from "webiny/admin/configs";
const { Theme } = AdminConfig;
const AdminTheme = () => {
return (
<AdminConfig.Public>
<Theme.Color palette={"primary"} color={"purple"} />
<Theme.Color palette={"secondary"} color={"green"} />
</AdminConfig.Public>
);
};
export default AdminTheme;
palette -- "primary", "secondary", "neutral", etc.color -- any CSS color value: named colors, hex ("#6B46C1"), or RGB.// extensions/AdminBranding/AdminTitleLogo.tsx
import React from "react";
import { AdminConfig } from "webiny/admin/configs";
import squareLogo from "./logo.png";
import horizontalLogo from "./logo.png";
const { Title, Logo } = AdminConfig;
const AdminTitleLogo = () => {
return (
<AdminConfig.Public>
<Title value={"ACME Corp"} />
<Logo
squareLogo={<img src={squareLogo} alt={"ACME Corp"} />}
horizontalLogo={<img src={horizontalLogo} alt={"ACME Corp"} />}
/>
</AdminConfig.Public>
);
};
export default AdminTitleLogo;
Register both:
<Admin.Extension src={"/extensions/AdminBranding/AdminTheme.tsx"} />
<Admin.Extension src={"/extensions/AdminBranding/AdminTitleLogo.tsx"} />
| Component | Purpose |
|---|---|
<Theme.Color palette="..." color="..." /> |
Set theme color palette |
<Title value="..." /> |
Set the Admin app title |
<Logo squareLogo={...} horizontalLogo={...} /> |
Set square and horizontal logos |
All must be wrapped in <AdminConfig.Public>.
Add custom columns to the content entry list view in the Admin UI. Columns can be restricted to specific content models.
// extensions/contactSubmission/EmailEntryListColumn.tsx
import React from "react";
import { ContentEntryListConfig } from "webiny/admin/cms/entry/list";
const { Browser } = ContentEntryListConfig;
// Custom cell component for the Email Type column
interface ContactSubmissionTableRow {
values: {
emailType: "work" | "personal";
};
}
export const EmailTypeCell = () => {
const { useTableRow, isFolderRow } = ContentEntryListConfig.Browser.Table.Column;
const { row } = useTableRow<ContactSubmissionTableRow>();
if (isFolderRow(row)) {
return <>{"-"}</>;
}
const emailType = row.data.values.emailType;
return emailType === "work" ? <>{"Business"}</> : <>{"Personal"}</>;
};
// Main extension component
const EmailEntryListColumn = () => {
return (
<ContentEntryListConfig>
{/* Simple column using path (no custom cell needed) */}
<Browser.Table.Column
name={"email"}
after={"name"}
path={"values.email"}
header={"Email"}
modelIds={["contactSubmission"]}
/>
{/* Custom cell column */}
<Browser.Table.Column
name={"emailType"}
after={"email"}
header={"Email Type"}
modelIds={["contactSubmission"]}
cell={<EmailTypeCell />}
/>
</ContentEntryListConfig>
);
};
export default EmailEntryListColumn;
Register:
<Admin.Extension src={"/extensions/contactSubmission/EmailEntryListColumn.tsx"} />
| Prop | Type | Description |
|---|---|---|
name |
string |
Unique column identifier |
header |
string |
Column header text |
path |
string |
Dot-path to the data field (e.g., "values.email") -- for simple columns |
cell |
ReactElement |
Custom React component for complex rendering |
modelIds |
string[] |
Restrict column to specific content models |
after |
string |
Position this column after another column by name |
Inside a custom cell component:
useTableRow<T>() -- access the full row data, typed with your interfaceisFolderRow(row) -- check if the current row is a folder (return placeholder content)Create custom forms for Website Builder page types using Webiny's form components:
// extensions/customPageTypes/RetailPageForm.tsx
import React from "react";
import { PageListConfig } from "webiny/admin/website-builder/page/list";
import { Bind, UnsetOnUnmount, validation } from "webiny/admin/form";
const { PageType } = PageListConfig;
export const RetailPageForm = () => {
const form = useForm();
return (
<>
{/* Mount the default page form fields. */}
<PageType.Language />
<PageType.Title />
<PageType.Path />
{/* Add custom fields.*/}
<Grid.Column span={12}>
<UnsetOnUnmount name={"extensions.customField"}>
<Bind name={"extensions.customField"} validators={[validation.create("required")]}>
<Input label={"Custom Field"} />
</Bind>
</UnsetOnUnmount>
</Grid.Column>
</>
);
};
| Component / Hook | Import | Purpose |
|---|---|---|
Bind |
"webiny/admin/form" |
Bind a form field to a name path |
useForm() |
"webiny/admin/form" |
Access the form API (getValue, setValue) |
validation |
"webiny/admin/form" |
Create validators (validation.create("required")) |
UnsetOnUnmount |
"webiny/admin/form" |
Clear the field value when the component unmounts |
Grid.Column |
"webiny/admin/ui" |
Layout grid column (span={12} for full width) |
Input |
"webiny/admin/ui" |
Text input field |
Select |
"webiny/admin/ui" |
Dropdown select with options |
FormApi |
"webiny/admin/form" |
Type for the form API object |
Register a dialog by name, open it from anywhere with params, and consume params inside the dialog.
// extensions/MyDialog.tsx
import React from "react";
import { AdminConfig } from "webiny/admin/configs";
import { useDialog } from "webiny/admin";
import { Dialog } from "webiny/admin/ui";
const MyDialog = () => {
const { params, closeDialog } = useDialog();
return (
<Dialog open onClose={closeDialog} title="My Dialog">
<p>Received param: {params.itemId as string}</p>
<Dialog.CancelAction onClick={closeDialog} text="Close" />
</Dialog>
);
};
const MyDialogConfig = () => {
return (
<AdminConfig>
<AdminConfig.Dialog name="my-dialog" element={<MyDialog />} />
</AdminConfig>
);
};
export default MyDialogConfig;
import { useOpenDialog } from "webiny/admin";
const MyButton = () => {
const { openDialog } = useOpenDialog();
return <button onClick={() => openDialog("my-dialog", { itemId: "abc" })}>Open Dialog</button>;
};
import { z } from "zod";
import { useDialog } from "webiny/admin";
const paramsSchema = z.object({ itemId: z.string() });
const MyDialog = () => {
const { params, closeDialog } = useDialog(paramsSchema);
// params.itemId is typed as string
};
| Hook / Component | Import | Purpose |
|---|---|---|
AdminConfig.Dialog |
"webiny/admin/configs" |
Register a named dialog |
useDialog(schema?) |
"webiny/admin" |
Read params inside a named dialog |
useOpenDialog(schema?) |
"webiny/admin" |
Open a named dialog with params |
useDialogs() |
"webiny/admin" |
Low-level access to openNamedDialog / closeNamedDialog |
Only one named dialog can be open at a time. Opening a new one replaces the current one.
Register a drawer (slide-in panel) by name, open it from anywhere with params. Multiple drawers can be open simultaneously — they stack on top of each other.
// extensions/MyDrawer.tsx
import React from "react";
import { AdminConfig } from "webiny/admin/configs";
import { useDrawer } from "webiny/admin";
import { Drawer } from "webiny/admin/ui";
const MyDrawer = () => {
const { params, closeDrawer } = useDrawer();
return (
<Drawer open onOpenChange={open => !open && closeDrawer()} title="My Drawer" size="md">
<p>Item: {params.itemId as string}</p>
</Drawer>
);
};
const MyDrawerConfig = () => {
return (
<AdminConfig>
<AdminConfig.Drawer name="my-drawer" element={<MyDrawer />} />
</AdminConfig>
);
};
export default MyDrawerConfig;
import { useOpenDrawer } from "webiny/admin";
const MyButton = () => {
const { openDrawer } = useOpenDrawer();
return <button onClick={() => openDrawer("my-drawer", { itemId: "abc" })}>Open Drawer</button>;
};
const { openDrawer } = useOpenDrawer();
// Open first drawer
openDrawer("settings-drawer", { tab: "general" });
// Open second drawer on top
openDrawer("detail-drawer", { id: "123" });
import { useDrawers } from "webiny/admin";
const { closeNamedDrawer } = useDrawers();
closeNamedDrawer(); // close the topmost drawer
closeNamedDrawer("my-drawer"); // close a specific drawer by name
import { z } from "zod";
import { useDrawer } from "webiny/admin";
const paramsSchema = z.object({ itemId: z.string() });
const MyDrawer = () => {
const { params, closeDrawer } = useDrawer(paramsSchema);
// params.itemId is typed as string
};
| Hook / Component | Import | Purpose |
|---|---|---|
AdminConfig.Drawer |
"webiny/admin/configs" |
Register a named drawer |
useDrawer(schema?) |
"webiny/admin" |
Read params inside a named drawer |
useOpenDrawer(schema?) |
"webiny/admin" |
Open a named drawer with params |
useDrawers() |
"webiny/admin" |
Low-level access to openNamedDrawer / closeNamedDrawer |
Key differences from dialogs:
closeNamedDrawer() without args closes the topmost; with a name closes that specific drawer"sm" (384px), "md" (520px), "lg" (640px), "xl" (1024px)Admin extensions can also add custom plugins to the Lexical rich text editor used in both the Headless CMS and the Website Builder. These are registered as <Admin.Extension> and use imports from "webiny/admin/lexical", "webiny/admin/cms/lexical", and "webiny/admin/website-builder/lexical".
White-label import: import { AdminConfig } from "webiny/admin/configs";
Data list import: import { ContentEntryListConfig } from "webiny/admin/cms/entry/list";
Form imports: import { Bind, useForm, validation } from "webiny/admin/form";
UI imports: import { Grid, Input, Select } from "webiny/admin/ui";
Register: <Admin.Extension src={"/extensions/MyAdminExtension.tsx"} />
Develop: yarn webiny watch admin
Deploy: yarn webiny deploy admin
webiny-project-structure -- How to register Admin extensionswebiny-full-stack-architect -- Full-stack extension skeleton and registrationwebiny-admin-architect -- Admin-side architecture patterns (headless + presentation features)