Web Forms

Rich content fields

Fields for rich text editing, static content display, fully custom components, and action buttons.


MarkdownEditor

A rich text editor that outputs both markdown and HTML. Powered by MDX Editor with a full toolbar including a heading picker (Normal / H1 / H2 / H3).

FormFieldClass.markdownEditor('description', {
  label: 'Description',
  required: true,
  helpText: 'Use markdown to format your content',
})

MarkdownEditor-specific options

OptionTypeDefaultDescription
placeholderstringPlaceholder text
heightnumber300Editor height in pixels
maxLengthnumberMaximum character count
disabledbooleanfalseDisables the editor
readOnlybooleanfalseRenders in read-only mode
readOnlyStyle'value' | 'disabled''value'How read-only is displayed
outputFormat'markdown' | 'html' | 'both''markdown'Output format
onHtmlChange(html: string) => voidCallback for HTML output
enableImageUploadbooleanfalseEnables image insertion
imageUploadHandler(file: File) => Promise<string>Custom upload handler
imageUploadMode'base64' | 'custom' | 'immediate''base64'How images are stored
maxImageSizenumber5242880Max image size in bytes (5 MB)
allowedImageTypesstring[]['image/png', 'image/jpeg', 'image/gif', 'image/webp']Accepted image types
overlayContainerHTMLElement | nullContainer for popups (fixes modal z-index conflicts)
popupZIndexnumberZ-index override for popups
pluginsPlugin[]Custom MDXEditor plugins — replaces all defaults

Features

  • Heading picker: Choose Normal, H1, H2, or H3 from the toolbar
  • Rich text toolbar: Bold, italic, underline, code, lists, links
  • Dual output: Stores both raw markdown and rendered HTML
  • Image upload: Supports base64 or custom upload handlers
  • Keyboard shortcuts: Standard shortcuts (Ctrl+B for bold, etc.)
  • Read-only mode: Display content without editing

Setup

The markdown editor requires an additional dependency:

npm install @mdxeditor/editor

Import the CSS in your application's entry point:

import '@mdxeditor/editor/style.css'

Image upload

FormFieldClass.markdownEditor('content', {
  label: 'Content',
  enableImageUpload: true,
  imageUploadMode: 'custom',
  imageUploadHandler: async (file) => {
    const formData = new FormData()
    formData.append('image', file)
    const { url } = await fetch('/api/upload', {
      method: 'POST',
      body: formData,
    }).then((r) => r.json())
    return url
  },
})

Dual format output

When outputFormat is 'both', the form data contains two keys: the field name for the raw markdown and {fieldName}_html for the rendered HTML.

FormFieldClass.markdownEditor('content', {
  label: 'Content',
  outputFormat: 'both',
  onHtmlChange: (html) => setRenderedHtml(html),
})

// Form data contains:
// content      → "# Hello\n\nThis is **bold**"
// content_html → "<h1>Hello</h1><p>This is <strong>bold</strong></p>"

HTML output is sanitized

The _html output is HTML-escaped, and link/image URLs are filtered to safe schemes (http, https, and mailto only), so script injection through markdown content is blocked.

Custom plugins

Pass plugins to take full control of the MDXEditor plugin set. When provided it replaces all defaults — you get exactly what you pass in.

import {
  headingsPlugin,
  listsPlugin,
  tablePlugin,
  toolbarPlugin,
  BlockTypeSelect,
  BoldItalicUnderlineToggles,
  InsertTable,
  Separator,
} from '@mdxeditor/editor'

FormFieldClass.markdownEditor('content', {
  label: 'Content',
  plugins: [
    headingsPlugin(),
    listsPlugin(),
    tablePlugin(),
    toolbarPlugin({
      toolbarContents: () => (
        <>
          <BlockTypeSelect />
          <Separator />
          <BoldItalicUnderlineToggles />
          <Separator />
          <InsertTable />
        </>
      ),
    }),
  ],
})

If the editor is inside a modal and link/image dialogs don't appear correctly, use one of these fixes:

// Option 1: render popups inside your modal container
FormFieldClass.markdownEditor('content', {
  overlayContainer: document.getElementById('my-modal'),
})

// Option 2: raise the popup z-index above your modal
FormFieldClass.markdownEditor('content', {
  popupZIndex: 10000,
})

Content

A display-only field for showing static content, instructions, or messages within a form. Content fields do not collect any data.

FormFieldClass.content('instructions', {
  label: 'Important',
  content:
    'Please fill out all required fields before submitting. Your information will be reviewed within 24 hours.',
})

Content-specific options

OptionTypeDefaultDescription
contentstring | ReactNodeThe content to display

Use cases

  • Section headers or dividers within a form
  • Contextual help or instructions
  • Legal text or disclaimers
  • Dynamic messages based on form state

Conditional content

FormFieldClass.content('warning', {
  label: 'Warning',
  content:
    'You are about to create a public listing. This action cannot be undone.',
  showWhen: (values) => values.visibility === 'public',
})

Custom

A fully custom field that renders your own React component while integrating with the form's state management and validation.

FormFieldClass.custom('colorPicker', {
  label: 'Brand Color',
  component: ({ value, onChange, error }) => (
    <div>
      <input
        type="color"
        value={value || '#000000'}
        onChange={(e) => onChange(e.target.value)}
      />
      {error && <span className="text-red-500">{error}</span>}
    </div>
  ),
  validate: (value) => value !== '#000000' || 'Please choose a color',
})

Custom-specific options

OptionTypeDefaultDescription
component(props) => JSX.ElementCustom React component

Component props

Your custom component receives:

PropTypeDescription
valueanyCurrent field value
onChange(value) => voidUpdate the field value
errorstring | undefinedCurrent validation error
disabledbooleanWhether the field is disabled
readOnlybooleanWhether the field is read-only

Advanced custom field

FormFieldClass.custom('fileUpload', {
  label: 'Upload Document',
  required: true,
  component: ({ value, onChange, error, disabled }) => (
    <div className="rounded-lg border-2 border-dashed p-6 text-center">
      {value ? (
        <div>
          <p>{value.name}</p>
          <button onClick={() => onChange(null)}>Remove</button>
        </div>
      ) : (
        <input
          type="file"
          disabled={disabled}
          onChange={(e) => onChange(e.target.files?.[0])}
          accept=".pdf,.doc,.docx"
        />
      )}
      {error && <p className="mt-2 text-red-500">{error}</p>}
    </div>
  ),
  validate: (value) => value != null || 'Please upload a document',
})

Button

An action button within the form. Useful for adding buttons that perform actions other than form submission.

FormFieldClass.button('addItem', {
  label: 'Add Line Item',
  onClick: () => {
    // Add a new item to the form
  },
})

Button-specific options

OptionTypeDefaultDescription
onClick() => voidClick handler
variantstringButton style variant

Use cases

const fields = [
  // ... other fields
  FormFieldClass.button('preview', {
    label: 'Preview',
    onClick: () => setShowPreview(true),
  }),
  FormFieldClass.button('reset', {
    label: 'Reset to Defaults',
    onClick: () => resetForm(),
  }),
]

Common patterns

Multi-section form with content dividers

const fields = [
  FormFieldClass.content('personalInfo', {
    content: 'Personal Information',
    wrapperClassName: 'col-span-2 font-bold text-lg border-b pb-2',
  }),
  FormFieldClass.text('firstName', {
    label: 'First Name',
    wrapperClassName: 'col-span-1',
  }),
  FormFieldClass.text('lastName', {
    label: 'Last Name',
    wrapperClassName: 'col-span-1',
  }),
  FormFieldClass.email('email', {
    label: 'Email',
    wrapperClassName: 'col-span-2',
  }),

  FormFieldClass.content('addressInfo', {
    content: 'Address Information',
    wrapperClassName: 'col-span-2 font-bold text-lg border-b pb-2 mt-4',
  }),
  FormFieldClass.text('street', {
    label: 'Street Address',
    wrapperClassName: 'col-span-2',
  }),
  FormFieldClass.text('city', {
    label: 'City',
    wrapperClassName: 'col-span-1',
  }),
  FormFieldClass.text('zip', {
    label: 'ZIP Code',
    wrapperClassName: 'col-span-1',
  }),
]

Rich text with character count

FormFieldClass.markdownEditor('article', {
  label: 'Article Body',
  required: true,
  maxLength: 10000,
  validate: (value) => {
    if (!value || value.length < 100) {
      return 'Article must be at least 100 characters'
    }
    return true
  },
})
Previous
Date & time fields