Building a React Page Builder: An Introduction to Puck
Tags: react, javascript, webdev
Published: 2023-09-28 09:43:00

---
title: Building a React Page Builder: An Introduction to Puck
published: true
description: An introduction to setting up Puck, the open-source visual editor for React
tags: react, javascript, webdev
cover_image: https://dev-to-uploads.s3.amazonaws.com/uploads/articles/myb1fg1hobyjte5f2pqe.png
# Use a ratio of 100:42 for best results.
published_at: 2023-09-28 09:43 +0000
canonical_url: https://measured.co/blog/building-a-react-page-builder-an-introduction-to-puck
---

Page builders are great. They fill a gap once supported by the WYSIWYG CMS, enabling no/low-code creation of pages in a headless CMS world.

However, we’ve never found a page builder that works for our clients. They’re normally:

1. Proprietary, making them impossible to use for our larger clients, or
2. Difficult to integrate, often requiring us to interface with a Java-based CMS or introduce convoluted integration layers

After years of searching for a self-hosted, React-first solution for our clients, we eventually decided to build our own.

## Introducing Puck

[Puck](https://github.com/measuredco/puck) is an open source page builder for React.

![Gif showing Puck interface](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/nykgnimed084co8b02d4.gif)

Puck enables you to create your own page builder and embed it directly inside your React application. Puck is:

- Open-source under MIT
- Self-hosted
- Compatible with most existing headless CMS solutions
- Extensible using plugins

Tell Puck what components you support, and Puck will provide a slick drag-and-drop interface for you or your content teams to create pages on-the-fly.

Puck also uses the permissive MIT license, so there’s nothing stopping you using it for commercial products.

Try the demo: [http://puck-editor-demo.vercel.app](http://puck-editor-demo.vercel.app/)

### The launch


Since Puck’s launch less than a month ago, we’ve been overwhelmed with the response.

We hit the [Hacker News front page](https://news.ycombinator.com/item?id=37391848), going from 8 to 1,800 stars on GitHub in 24 hours, continuing to grow to over 3,000 throughout September. Industry heavyweights like [Simon Willison](https://news.ycombinator.com/item?id=37393639) (co-creator of Django) and [Guillermo Rauch](https://twitter.com/rauchg/status/1699106822698025103) (creator of Next.js and CEO of Vercel) both gave their praise.

{% embed https://twitter.com/rauchg/status/1699106822698025103 %}

### A quick look at the Puck API

Puck is React-first, and its core functionality is exposed via the `<Puck>` React component.

To render the Puck editor:

```tsx
// Editor.jsx
import { Puck } from "@measured/puck";
import "@measured/puck/dist/index.css";

// Puck configuration, describing components to drag-and-drop
const config = {
  components: {
    HeadingBlock: {
      // Field types to render for each prop of your component
      fields: {
        children: {
          type: "text",
        },
      },
      // Your render function
      render: ({ children }) => {
        return <h1>{children}</h1>;
      },
    },
  },
};

// Describe the initial data
const initialData = {};

// Save the data to your database
const save = (data) => {};

// Render Puck editor
export function Editor() {
  return <Puck config={config} data={initialData} onPublish={save} />;
}
```

To render a page:

```jsx
// Page.jsx
import { Render } from "@measured/puck";

export function Page() {
  return <Render config={config} data={data} />;
}
```

## Setup

### Installing Puck

If you have an existing application, you can install Puck via npm:

```
npm install @measured/puck
```

Or if you’re interested in spinning up a new application, you can run the generator

```
npm create puck-app
```

For this guide, we’re going to assume you’ve used the generator and chosen the [next recipe](https://github.com/measuredco/puck/tree/main/recipes/next), but most of it will apply to existing applications too.

### Running the application

If you’re using the next recipe, navigate into your directory and run the application:

```
cd my-app
npm run dev
```

The server will start on port **3000**. If you navigate to the homepage `/`, you’ll be prompted to redirect to the editor.

If you now navigate to `edit` you’ll see Puck rendering with a single component, **HeadingBlock**.

![The default Puck interface](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ualz1i6lbx0oddnievxk.png)

This is the Puck editor for our homepage, which contains a single **HeadingBlock**. If we click the component, we can update the text being rendered using the field on the right hand side.

![The default Puck interface with HeadingBlock updated to read Hello World](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/bua1or4bdis73gend52i.png)

If you hit **Publish** in the top-right corner, our homepage at  `/` will be updated.

**You can add `/edit` to *any* route in your application to spin up a Puck editor, whether or not the page already exists. This is thanks to the Next.js catch-all route under `app/[…puckPath]`.**

## Understanding the Puck config

Let’s take a look at the `puck.config.tsx` file generated by the generator:

```tsx
// puck.config.tsx
import type { Config } from "@measured/puck";

type Props = {
  HeadingBlock: { title: string };
};

export const config: Config<Props> = {
  components: {
    HeadingBlock: {
      fields: {
        title: { type: "text" },
      },
      defaultProps: {
        title: "Heading",
      },
      render: ({ title }) => (
        <div style={{ padding: 64 }}>
          <h1>{title}</h1>
        </div>
      ),
    },
  },
};

export default config;
```

This TypeScript file is pretty similar to the API example we shared above, with some type declarations. It exports a `config` object that matches Puck’s `Config` type.

Let’s break it down —

```tsx
type Props = {
  HeadingBlock: { title: string };
};
```

This type definition allows us to tell Puck’s `Config` type what our underlying components are expecting. In this scenario, we’re defining one component, **HeadingBlock**, that accepts props `title`, which is a `string`.

We then pass this to our `config`, which tells Puck that we’re expecting to render a component called HeadingBlock that accepts the `title: string` prop.

```tsx
export const config: Config<Props> = {
  components: {
    HeadingBlock: {
      fields: {
        title: { type: "text" },
      },
      defaultProps: {
        title: "Heading",
      },
      render: ({ title }) => (
        <div style={{ padding: 64 }}>
          <h1>{title}</h1>
        </div>
      ),
    },
  },
};
```

We use the extended `Config` type definition to create a `config` object. Our `config` object accepts the `components` key (see [API reference](https://github.com/measuredco/puck#config)), which must now define a **HeadingBlock** configuration.

Our **HeadingBlock** definition receives three keys: `fields`, `defaultProps` and `render`.

```tsx
fields: {
  title: { type: "text" },
},
```

`fields` describes how to render the field for each prop passed to our component. In this case, we’re using a **text** field. See the [field API reference](https://github.com/measuredco/puck#field) for a full list of available field types.

```tsx
defaultProps: {
  title: "Heading",
},
```

Because we’ve specified this as a required field, TypeScript will complain if we don’t configure default values for the props. Here, we’re setting the `title` to **Heading**.

> We have to use the defaultProps key to set defaults so that Puck knows how to render the data in the fields.
> 

```tsx
render: ({ title }) => (
  <div style={{ padding: 64 }}>
    <h1>{title}</h1>
  </div>
),
```

Finally, we define a `render` function that tells Puck how to render the component.

## Adding a Paragraph component

Let’s add a new component called **Paragraph** that accepts a `text: string` prop and renders a Paragraph element.

```tsx
type Props = {
  HeadingBlock: { title: string };
  Paragraph: { text: string };
};

export const config: Config<Props> = {
  components: {
    //...existingComponents,

    Paragraph: {
      fields: {
        text: { type: "text" },
      },
      defaultProps: {
        text: "Paragraph",
      },
      render: ({ text }) => (
        <div style={{ padding: 64 }}>
          <p>{text}</p>
        </div>
      ),
    },
  },
};
```

We can now see this in the left hand side of the editor:

![Puck interface showing Paragraph component in the left-hand side](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/lpl6f9lgximvy6m3z46f.png)

Let’s drag it onto our page and update it:

![Gif showing the user dragging the Paragraph into the page](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/gamr168brwf8vygn98gh.gif)

Great! Let’s add a text alignment property called `textAlign`, which accepts the values `left`, `center` or `right`.

```tsx
type Props = {
  HeadingBlock: { title: string };
  Paragraph: {
    text: string;
    textAlign: "left" | "center" | "right";
  };
};
```

To render this, a `radio` field type would be better than a `text` input because it doesn’t allow for the input of arbitrary text:

```tsx
export const config: Config<Props> = {
  components: {
    // ...existingComponents

    Paragraph: {
      fields: {
        text: { type: "text" },
        textAlign: {
          type: "radio",
          options: [
            { label: "Left", value: "left" },
            { label: "Center", value: "center" },
            { label: "Right", value: "right" },
          ],
        },
      },
      defaultProps: {
        text: "Paragraph",
        textAlign: "left",
      },
      render: ({ text, textAlign }) => (
        <div style={{ padding: 64 }}>
          <p style={{ textAlign }}>{text}</p>
        </div>
      ),
    },
  },
};
```

We’ve also set a default value of `left`, and mapped it straight to the `textAlign` CSS on our `<p>` element.

Deleting our existing Paragraph and drag a new one in shows us the new field on the right hand side. Toggling between the options works as expected!

![Puck interface with Paragraph center-aligned](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/cewelzmt7i2a2rumourn.png)

## Adding Tailwind

Let’s add Tailwind CSS to improve the styles:

```
npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init -p
```

Add the Tailwind classes to `app/styles.css`:

```css
@tailwind base;
@tailwind components;
@tailwind utilities;
```

Configure `tailwind.config.js` to process the Puck config:

```
/** @type {import('tailwindcss').Config} */
module.exports = {
  content: ["puck.config.tsx"],
  theme: {
    extend: {},
  },
  plugins: [],
};
```

Finally, let’s add some classes to our **HeadingBlock** and remove the padding on the wrapper:

```tsx
    render: ({ title }) => (
      <div>
        <h1 className="text-4xl lg:text-5xl font-extrabold tracking-tight">
          {title}
        </h1>
      </div>
    ),
```

And our **Paragraph**:

```tsx
    render: ({ text, textAlign }) => (
      <div>
        <p style={{ textAlign }} className="leading-7 mt-6">
          {text}
        </p>
      </div>
    ),
```

Let’s see how that looks:

![Puck interface showing preview window with Tailwind for the HeadingBlock and Paragraph](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/abpteqh8bff1oix1xvbq.png)

Great! The styles are rendering, but things are looking a little cramped.

## Rendering a custom root

To fix this, we’ll want to add a custom `root`. Puck allows us to change the default behaviour by overriding the `root` component of our page:

```tsx
export const config: Config<Props> = {
  ... existingConfig

  root: {
    render: ({ children }) => {
      return <div className="p-16">{children}</div>;
    },
  },
}
```

This time we’re using Tailwind instead of inline styles.

![Puck interface showing preview with additional padding wrapping the root of the preview](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/8xa2qqe82xv4e0ddhgfi.png)

Much better!

## Taking it further

There are a bunch of Puck features that we won't cover in this tutorial that you could use to take your editor to the next level:

* **Adaptors** - allow your users to select content from an existing headless CMS using adaptors
* **Plugins** - the custom plugin API enables powerful new functionality. For example, the [`heading-analyzer`](https://github.com/measuredco/puck/tree/main/packages/plugin-heading-analyzer) plugin to analyse the document structure of your page and warn when you break WCAG2.1 guidelines
* **Custom fields** - create your own input types for your users, like calendar widgets

You'll find documentation on all of these via [our GitHub](https://github.com/measuredco/puck).

## Closing words

We are committed to continuing the development of Puck, with features like multi-column layouts, more form inputs and new plugins in the works. After all, we need it for our clients too.

In the last month, our community has started building bespoke website builders, email builders and other innovative applications we never could have imagined.

If you like Puck, please throw us a star via our [GitHub repository](https://github.com/measuredco/puck) or come chat on our [Discord server](https://discord.gg/D9e4E3MQVZ). For more information about our React services, please visit the [Measured website](https://measured.co/).

Thank you for joining us on this journey. We're excited to see what you create with Puck!