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
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,
};attributeRegionsturns thetitleattribute into an inline editable region.childrenRegionturns the children into a block region: paragraphs, lists, code, nested components.propslists the remaining attributes for the attributes panel. Abooleanprop becomes a toggle.insertis the fragment the slash menu inserts.emptyComponentbuilds it: every region present, all empty.regionsgives each region a class name. Regions render outside the renderer's tree, so this is where their typography lives.
Draw the chrome
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
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 targetdata-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 prop | Holds | Example |
|---|---|---|
props | plain-string attributes | type="info" → props.type === "info" |
literals | static values of expression attributes | soon={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