DocsIntegrating PuckRich Text Editing

Rich Text Editing

Puck supports rich text editing via the richtext field.

Adding a rich text field

To use rich text, add the richtext field to your component config:

const config = {
  components: {
    Example: {
      fields: {
        body: {
          type: "richtext",
        },
      },
      render: ({ body }) => body,
    },
  },
};
Interactive Demo
Example

Enabling inline editing

Set contentEditable to make the field editable inline:

const config = {
  components: {
    Example: {
      fields: {
        body: {
          type: "richtext",
          contenEditable: true,
        },
      },
      render: ({ body }) => body,
    },
  },
};
Interactive Demo
Example

Customizing behavior

The rich text field is built with Tiptap, and comes with several Tiptap extensions pre-configured. Editor behavior can be customized by configuring these extensions.

Disabling functionality

Disable any extension by setting it to false in options:

const config = {
  components: {
    Example: {
      fields: {
        body: {
          type: "richtext",
          options: {
            // Disable bold extension
            bold: false,
          },
        },
      },
      render: ({ body }) => body,
    },
  },
};
Interactive Demo
Example

Configuring extensions

Provide configuration options for the included extensions directly to options for further customization:

const config = {
  components: {
    Example: {
      fields: {
        body: {
          type: "richtext",
          options: {
            // Restrict headings to h1 and h2
            // https://tiptap.dev/docs/editor/extensions/nodes/heading
            heading: { levels: [1, 2] },
          },
        },
      },
      render: ({ body }) => body,
    },
  },
};
Interactive Demo
Example

Customizing the menu bar

Use renderMenu() to customize the menu bar layout using the <RichTextMenu> component:

import { RichTextMenu } from "@measured/puck";
 
const config = {
  components: {
    Example: {
      fields: {
        body: {
          type: "richtext",
          renderMenu: () => (
            {/*  Always wrap in <RichTextMenu> */}
            <RichTextMenu>
              {/* Group items in <RichTextMenu.Group> */}
              <RichTextMenu.Group>
                {/* Only include the bold control */}
                <RichTextMenu.Bold />
              </RichTextMenu.Group>
            </RichTextMenu>
          ),
        },
      },
      render: ({ body }) => body,
    },
  },
};
Interactive Demo
Example

Hiding a control does not disable the functionality. Users can still use keyboard shortcuts and other input mechanisms. To disable functionality, use options.

A full list of controls is available in the included controls API reference.

Adding custom extensions

Configure the extension

Provide additional Tiptap extensions to the tiptap.extensions parameter to introduce new functionality:

import Superscript from "@tiptap/extension-superscript";
 
const config = {
  components: {
    Example: {
      fields: {
        body: {
          type: "richtext",
          tiptap: {
            extensions: [Superscript],
          },
        },
      },
      render: ({ body }) => body,
    },
  },
};

Add a new control

If you’re adding a control for a new extension, first update the tiptap.selector to make the state available to the menu:

import Superscript from "@tiptap/extension-superscript";
 
const config = {
  components: {
    Example: {
      fields: {
        body: {
          type: "richtext",
          tiptap: {
            extensions: [Superscript],
            selector: ({ editor }) => ({
              isSuperscript: editor?.isActive("superscript"),
              canSuperscript: editor?.can().chain().toggleSuperscript().run(),
            }),
          },
        },
      },
      render: ({ body }) => body,
    },
  },
};

Then implement a new control with <RichTextMenu.Control> and render it with renderMenu():

import Superscript from "@tiptap/extension-superscript";
import { Superscript } from "lucide-react";
import { RichTextMenu } from "@measured/puck";
 
const config = {
  components: {
    Example: {
      fields: {
        body: {
          type: "richtext",
          // ...
          renderMenu: ({ children, editor, editorState }) => (
            <RichTextMenu>
              {/* Render the default controls */}
              {children}
 
              <RichTextMenu.Group>
                <RichTextMenu.Control
                  icon={<Superscript />}
                  onClick={() =>
                    editor?.chain().focus().toggleSuperscript().run()
                  }
                  active={editorState?.isSuperscript}
                  disabled={!editorState?.canSuperscript}
                />
              </RichTextMenu.Group>
            </RichTextMenu>
          ),
        },
      },
      render: ({ body }) => body,
    },
  },
};

Further reading