Quick start
Install the plugin, author a form in the admin, and render it on your frontend.
Install
pnpm add @10x-media/form-builderConfigure
import { buildConfig } from 'payload'
import { lexicalEditor } from '@payloadcms/richtext-lexical'
import { formBuilder } from '@10x-media/form-builder'
export default buildConfig({
// ...
editor: lexicalEditor(),
plugins: [formBuilder()],
})The plugin's notification bodies are rich text fields that inherit your project's editor, so the config must set one (any editor works; Payload throws MissingEditorProp at startup otherwise). See rich text bodies.
This registers two collections under a Forms admin group: forms (the documents editors author) and form-submissions (typed, validated responses). File uploads are opt-in and bring-your-own: point uploads: { collection: '...' } at an upload collection you own to enable the file field type (see File uploads).
The in-admin condition and flow builders ship their own styles; no admin setup is needed.
formBuilder registers with order: 50. Plugins execute in ascending order (lower first), so any plugin that needs to run after it, for example @payloadcms/plugin-multi-tenant, which wraps every collection already present in the config including the three above, needs an explicit order higher than 50.
Author a form
In the admin, create a forms document: give it a title, then two checkboxes, Multi-step and Poll, decide which tabs join Fields: Flow (the step builder) only when Multi-step is on, Poll (poll configuration) only when Poll is on. Leave both off for an ordinary single-page form. Add fields as blocks in the Fields tab (a text field name, an email field email, a textarea message); the submit button's label sits at the bottom of that same tab, and, once Multi-step is on, the previous/next labels sit at the bottom of the Flow tab. Actions (post-submit) and Response (the success message, or a redirect) round out the tabs. Each field block is tabbed too: Field holds the basics and type config, Validation the rules, Advanced the visibility condition and hidden flag.
The title is the document's own, for finding the form in the admin, required in your project's default locale (following your project's localization, if any) so other locales fall back to it. The plugin never renders it: <Form> draws the fields and nothing above them, and form.title is there for your page to render or ignore.
Render it
Fetch the form document and hand it to the headless renderer:
import { getPayload } from 'payload'
import config from '@payload-config'
import { toFormDocument } from '@10x-media/form-builder/rsc'
import { ContactForm } from './ContactForm'
export default async function ContactPage() {
const payload = await getPayload({ config })
const { docs } = await payload.find({
collection: 'forms',
where: { title: { equals: 'Contact' } },
limit: 1,
})
return <ContactForm form={toFormDocument(docs[0])} />
}toFormDocument narrows the Payload-generated document to the FormDocument shape the renderer expects, without a cast; it is server-safe, so calling it in a Server Component is fine.
'use client'
import { Form, type FormDocument } from '@10x-media/form-builder/react'
import '@10x-media/form-builder/styles.css'
export function ContactForm({ form }: { form: FormDocument }) {
return <Form form={form} onSuccess={(id) => console.log('submitted', id)} />
}<Form> renders the fields with its built-in unstyled renderers, validates progressively, and POSTs to /api/form-submissions.
Verify
Submit the form. The submission appears in Form Submissions with a formatted answers view; the server re-validated every value before storing it. Leave a required field empty and submit: the server rejects it and the error lands on the right field.
Next
- Make it look right: Styling (CSS hooks, the shadcn registry, or custom renderers).
- Add rules and logic: Validation and Conditions.
- Do something with submissions: Actions.
- A complete working example (multi-step form and poll) lives in the package's
dev/app/(frontend)/directory in the repository.