The self-hosted, drag and drop editor for React.
- 🖱️ Drag and drop: Visual editing for your existing React component library
- 🌐 Integrations: Load your content from a 3rd party headless CMS
- ✍️ Inline editing: Author content directly via puck for convenience
- ⭐️ No vendor lock-in: Self-host or integrate with your existing application
Render the editor:
// Editor.jsx
import { Puck } from "@measured/puck";
import "@measured/puck/dist/index.css";
// Create puck component config
const config = {
components: {
HeadingBlock: {
fields: {
children: {
type: "text",
},
},
render: ({ children }) => {
return <h1>{children}</h1>;
},
},
},
};
// Describe the initial data
const initialData = {
content: [],
root: {},
};
// Save the data to your database
const save = (data) => {};
// Render Puck editor
export function Editor() {
return <Puck config={config} data={initialData} onPublish={save} />;
}
Render the page:
// Page.jsx
import { Render } from "@measured/puck";
import "@measured/puck/dist/index.css";
export function Page() {
return <Render config={config} data={data} />;
}
Install the package
npm i @measured/puck --save
Or generate a puck application using a recipe
npx create-puck-app my-app
Puck is a React component that can be easily integrated into your existing application. We also provide helpful recipes for common use cases:
- next: Next.js app example
Puck can be configured to work with plugins. Plugins can extend the functionality to support novel functionality.
heading-analyzer
: Analyze the heading outline of your page and be warned when you're not respecting WCAG 2 accessibility standards.
The plugin API follows a React paradigm. Each plugin passed to the Puck editor can provide three functions:
renderRoot
(Component
): Render the root node of the preview contentrenderRootFields
(Component
): Render the root fieldsrenderFields
(Component
): Render the fields for the currently selected componentrenderComponentList
(Component
): Render the component list
Each render function receives three props:
- children (
ReactNode
): The normal contents of the root or field. You must render this if provided. - state (
AppState
): The current application state, including data and UI state - dispatch (
(action: PuckAction) => void
): The Puck dispatcher, used for making data changes or updating the UI. See the action definitions for a full reference of available mutations.
Here's an example plugin that creates a button to toggle the left side-bar:
const myPlugin = {
renderRootFields: ({ children, dispatch, state }) => (
<div>
{children}
<button
onClick={() => {
dispatch({
type: "setUi",
ui: { leftSideBarVisible: !state.ui.leftSideBarVisible },
});
}}
>
Toggle side-bar
</button>
</div>
),
};
Puck supports custom fields using the custom
field type and render
method.
In this example, we optionally add the <FieldLabel>
component to add a label:
import { FieldLabel } from "@measured/puck";
export const MyComponent: ComponentConfig = {
fields: {
myField: {
type: "custom",
render: ({ field, name, onChange, value }) => {
return (
<FieldLabel label={field.label || name}>
<input
placeholder="Enter text..."
type="text"
name={name}
defaultValue={value}
onChange={(e) => onChange(e.currentTarget.value)}
></input>
</FieldLabel>
);
},
},
},
};
Puck supports creating complex layouts (like multi-column layouts) using the <DropZone>
component.
In this example, we use the <DropZone>
component to render two nested DropZones within another component:
import { DropZone } from "@measured/puck";
export const MyComponent: ComponentConfig = {
render: () => {
return (
<div>
<DropZone zone="first-drop-zone">
<DropZone zone="second-drop-zone">
</div>
)
}
};
You can also do this at the root of your component. This is useful if you have a fixed layout and only want to make certain parts of your page customisable:
import { DropZone, Config } from "@measured/puck";
export const config: Config = {
root: {
render: ({ children }) => {
return (
<div>
{/* children renders the default zone. This can be omitted if necessary. */}
{children}
<div>
<DropZone zone="other-drop-zone">
</div>
</div>
)
}
}
};
The current DropZone implementation has certain rules and limitations:
- You can drag from the component list on the LHS into any DropZone
- You can drag components between DropZones, so long as those DropZones share a parent (also known as area)
- You can't drag between DropZones that don't share a parent (or area)
- Your mouse must be directly over a DropZone for a collision to be detected
Adaptors can be used to import data from a third-party API, such as a headless CMS.
The external
field type enables us to use an adaptor to query data from a third party API:
const myAdaptor = {
name: "My adaptor",
fetchList: async () => {
const response = await fetch("https://www.example.com/api");
return {
text: response.json().text,
};
},
};
const config = {
components: {
HeadingBlock: {
fields: {
myData: {
type: "external",
adaptor: myAdaptor,
},
},
render: ({ myData }) => {
return <h1>{myData.text}</h1>;
},
},
},
};
When the user interacts with this adaptor, they'll be presented with a list of items to choose from. Once they select an item, the value will be mapped onto the prop. In this case, myData
.
Dynamic prop resolution allows developers to resolve props for components without saving the data to the Puck data model.
resolveProps
is defined in the component config, and allows the developer to make asynchronous calls to change the props after they've been set by Puck.
- props (
object
): the current props for your component stored in the Puck data
- props (
object
): the resolved props for your component. Will not be stored in the Puck data - readOnly (
object
): an object describing which fields on the component are currently read-only- [prop] (
boolean
): boolean describing whether or not the prop field is read-only
- [prop] (
In this example, we remap the text
prop to the title
prop and mark the title
field as read-only.
const config = {
components: {
HeadingBlock: {
fields: {
text: {
type: "text",
},
title: {
type: "text",
},
},
resolveProps: async (props) => {
return {
props: {
title: props.text,
},
readOnly: {
title: true,
},
};
},
render: ({ title }) => {
return <h1>{title}</h1>;
},
},
},
};
A more advanced pattern is to combine the resolveProps
method with the adaptors to dynamically fetch data when rendering the component.
const myAdaptor = {
name: "My adaptor",
fetchList: async () => {
const response = await fetch("https://www.example.com/api");
return {
id: response.json().id,
};
},
};
const config = {
components: {
HeadingBlock: {
fields: {
myData: {
type: "external",
adaptor: myAdaptor,
},
title: {
type: "text",
},
},
resolveProps: async (props) => {
if (!myData.id) {
return { props, readOnly: { title: false } };
}
const latestData = await fetch(
`https://www.example.com/api/${myData.id}`
);
return {
props: {
title: latestData.json().text,
},
readOnly: {
title: true,
},
};
},
render: ({ title }) => {
return <h1>{title}</h1>;
},
},
},
};
resolveData
is a utility function exported by Puck to enable the developer to resolve their custom props before rendering their component with <Render>
. This is ideally done on the server. If you're using resolveProps
, you must use resolveData
before rendering.
import { resolveData } from "@measured/puck";
const resolvedData = resolveData(data, config);
The <Puck>
component renders the Puck editor.
- config (
Config
): Puck component configuration - data (
Data
): Initial data to render - onChange (
(Data) => void
[optional]): Callback that triggers when the user makes a change - onPublish (
(Data) => void
[optional]): Callback that triggers when the user hits the "Publish" button - renderComponentList (
Component
[optional]): Render function for wrapping the component list - renderHeader (
Component
[optional]): Render function for overriding the Puck header component - renderHeaderActions (
Component
[optional]): Render function for overriding the Puck header actions. Use a fragment. - headerTitle (
string
[optional]): Set the title shown in the header title - headerPath (
string
[optional]): Set a path to show after the header title - plugins (
Plugin[]
[optional]): Array of plugins that can be used to enhance Puck
The <Render>
component renders user-facing UI using Puck data.
- config (
Config
): Puck component configuration - data (
Data
): Data to render
The <DropZone>
component allows you to create advanced layouts, like multi-columns.
- zone (
string
): Identifier for the zone of your component, unique to the parent component - style (
CSSProperties
): Custom inline styles
The Config
object describes which components Puck should render, how they should render and which inputs are available to them.
- root (
object
)- fields (
object
):- title (
Field
): Title of the content, typically used for the page title. - [fieldName] (
Field
): User defined fields, used to describe the input data stored in theroot
key.
- title (
- render (
Component
): Render a React component at the root of your component tree. Useful for defining context providers.
- fields (
- components (
object
): Definitions for each of the components you want to show in the visual editor- [componentName] (
object
)- fields (
Field
): The Field objects describing the input data stored against this component. - render (
Component
): Render function for your React component. Receives props as defined in fields. - defaultProps (
object
[optional]): Default props to pass to your component. Will show in fields. - resolveProps (
async (props: object) => object
[optional]): Function to dynamically change props before rendering the component.- Args
- props (
object
): the current props for your component stored in the Puck data
- props (
- Response
- props (
object
): the resolved props for your component. Will not be stored in the Puck data - readOnly (
object
): an object describing which fields on the component are currently read-only- [prop] (
boolean
): boolean describing whether or not the prop field is read-only
- [prop] (
- props (
- Args
- fields (
- [componentName] (
- categories (
object
): Component categories for rendering in the side bar or restricting in DropZones- [categoryName] (
object
)- components (
sting[]
, [optional]): Array containing the names of components in this category - title (
sting
, [optional]): Title of the category - visible (
boolean
, [optional]): Whether or not the category should be visible in the side bar - defaultExpanded (
boolean
, [optional]): Whether or not the category should be expanded in the side bar by default
- components (
- [categoryName] (
A Field
represents a user input field shown in the Puck interface.
- type (
text
|textarea
|number
|select
|radio
|external
|array
|custom
): The input type to render - label (
text
[optional]): A label for the input. Will use the key if not provided. - arrayFields (
object
): Object describing sub-fields for items in anarray
input- [fieldName] (
Field
): The Field objects describing the input data for each item
- [fieldName] (
- getItemSummary (
(object, number) => string
[optional]): Function to get the name of each item when using thearray
orexternal
field types - defaultItemProps (
object
[optional]): Default props to pass to each new item added, when using aarray
field type - options (
object[]
): array of items to render for select or radio inputs- label (
string
) - value (
string
|number
|boolean
)
- label (
- adaptor (
Adaptor
): Content adaptor if using theexternal
input type - adaptorParams (
object
): Paramaters passed to the adaptor - render (
Component
): Render a custom field. Receives the props:- field (
Field
): Field configuration - name (
string
): Name of the field - value (
any
): Value for the field - onChange (
(value: any) => void
): Callback to change the value - readOnly (
boolean
|undefined
): Whether or not the field should be in readOnly mode
- field (
The AppState
object stores the puck application state.
- data (
Data
): The page data currently being rendered - ui (
object
):- leftSideBarVisible (boolean): Whether or not the left side bar is visible
- itemSelector (object): An object describing which item is selected
- arrayState (object): An object describing the internal state of array items
- componentList (object): An object describing the component list. Similar shape to
Config.categories
.- components (
sting[]
, [optional]): Array containing the names of components in this category - title (
sting
, [optional]): Title of the category - visible (
boolean
, [optional]): Whether or not the category is visible in the side bar - expanded (
boolean
, [optional]): Whether or not the category is expanded in the side bar
- components (
The Data
object stores the puck page data.
- root (
object
):- title (string): Title of the content, typically used for the page title
- [prop] (string): User defined data from
root
fields
- content (
object[]
):- type (string): Component name
- props (object):
- [prop] (string): User defined data from component fields
An Adaptor
can be used to load content from an external content repository, like Strapi.js.
- name (
string
): The human-readable name of the adaptor - fetchList (
(adaptorParams: object) => object
): Fetch a list of content and return an array
Plugins that can be used to enhance Puck.
- renderRoot (
Component
): Render the root node of the preview content - renderRootFields (
Component
): Render the root fields - renderFields (
Component
): Render the fields for the currently selected component
Puck is developed and maintained by Measured, a small group of industry veterans with decades of experience helping companies solve hard UI problems. We offer consultancy and development services for scale-ups, SMEs and enterprises.
If you need support integrating Puck or creating a beautiful component library, please reach out via our website.
MIT © Measured Co.