Fumadocs Editor

Component Authoring

Make your own MDX components editable in place

A component without a spec already round-trips byte-for-byte: it renders as a neutral card and is edited as source. A spec is what turns <Feature title="…"> into a block edited in place, with its own slash-menu entry and attributes panel.

Why a spec

The editor never renders your real components. Each one gets two things instead:

  • a spec: data describing how attributes and children map to editable regions
  • a renderer: a lightweight mirror that draws the look around those regions

Because the spec is data, the editor keeps the declared shape on every edit, and a renderer that throws falls back to a source card without losing the document.

A worked example

Take a <Feature title="…" soon={true}> card.

Declare the shape

feature.tsx
export const featureSpec: UiComponentSpec = {
  name: "Feature",
  label: "Feature",
  attributeRegions: [{ attribute: "title", region: "title", placeholder: "Feature name…" }],
  childrenRegion: { region: "body", placeholder: "Describe the feature…" },
  props: [{ name: "soon", label: "Coming soon", type: "boolean", default: false }],
  // regions render outside the renderer's tree; style them by name
  regions: { title: "font-medium" },
  insert: (specs) => emptyComponent(featureSpec, specs),
  render: Feature,
};
  • attributeRegions turns the title attribute into an inline editable region.
  • childrenRegion turns the children into a block region: paragraphs, lists, code, nested components.
  • props lists the remaining attributes for the attributes panel. A boolean prop becomes a toggle.
  • insert is the fragment the slash menu inserts. emptyComponent builds it: every region present, all empty.
  • regions gives each region a class name. Regions render outside the renderer's tree, so this is where their typography lives.

Draw the chrome

feature.tsx
function Feature({ literals, children }: ComponentRenderProps) {
  const soon = literals.soon === true;
  return (
    <div className="rounded-xl border border-fd-border bg-fd-card p-4">
      {soon && (
        <span
          contentEditable={false}
          className="float-right rounded-full bg-fd-primary/10 px-2 py-0.5 text-xs text-fd-primary"
        >
          Soon
        </span>
      )}
      {children}
    </div>
  );
}

children holds the editable regions. Everything else is chrome. Style it however your app styles things: the editor's own chrome is StyleX and never depends on your CSS setup.

Register it

editor.tsx
const components = [...fumadocsUiComponents, featureSpec];

export function Editor({ text }: { text: string }) {
  return <MdxEditor defaultValue={text} components={components} />;
}

Every <Feature> in a document is now editable in place.

Regions

children in the render props is all regions, in document order: here the title inline region, then the body block region. You place them once; there is no separate slot per region.

  • Style a region by name with regions, or target data-region="<name>" from the wrapper.
  • Mark chrome contentEditable={false} (the "Soon" badge) so the caret cannot enter it. Double-clicking chrome selects the component, so give every component some: an icon, a rail, a padded edge.
  • A component that nests in another (a row, a card) reserves a spot for the touch joystick in its own chrome: an empty element with data-fde-controls. The built-in rows, cards, steps, tabs and accordions do.

Props and literals

JSX attributes reach the renderer in two forms:

Render propHoldsExample
propsplain-string attributestype="info"props.type === "info"
literalsstatic values of expression attributessoon={true}literals.soon === true

setProp(name, value) writes a string attribute. setLiteral(name, value) rewrites an expression from a literal value; the source text is derived. Use them for controls the renderer owns, like the Callout type picker. An expression that is not a static literal (a variable, a call) stays out of literals and is edited as source in the attributes panel.

Spec reference

Structural fields on ComponentSpec (from @fumadocs-editor/core):

Prop

Type

UiComponentSpec (from @fumadocs-editor/ui) adds the UI half:

Prop

Type

contentRegion, childrenRegion and childComponent are mutually exclusive. A spec with none of them is a leaf: props only, like GithubInfo. For containers, the built-in specs are the reference: Cards and Card, Files (listLike), Tabs (itemsAttribute).

Last updated on

On this page