Skip to main content
Version: 3.x

Create custom sections

Sections are the building blocks a content manager assembles into a page. This guide explains what a section is made of and how to add your own.

Adding a section type takes three steps: define its editable fields, render them with a React component, and register the section so pages can use it and the editor can offer it. This guide walks through each.

What a section is made of

A section has two halves that share the same type:

  1. A definition — its metadata (label, icon, category, defaultProps) and a schema describing the editable fields (see Section field types for every available type).
  2. A React component — registered in the CmsSection composition scope and receiving the section's props. The common options (background, spacing, visibility) are applied around it automatically, so your component only renders its own props.

Built-in sections can be used as reference.

Define the editable fields

A section is described by its composition metadata: a sibling file next to the component, named <Component>.CompositionMetadata.ts, that exports <Scope>CompositionMetadata — for the CmsSection scope, CmsSectionCompositionMetadata. It declares the picker entry (label, icon, category), the defaultProps a freshly added section starts with, and the schema — the editable fields and the editor control each one uses:

app/cms/sections/Callout/Callout.CompositionMetadata.ts
import type { CmsSectionMetadata } from "@front-commerce/cms";

export const CmsSectionCompositionMetadata = {
name: "Call to action",
description: "A heading, a short message and a call-to-action button",
icon: "📣",
category: "content",
defaultProps: {
title: {
text: "Need help?",
level: "h2",
size: "text-3xl",
weight: "font-bold",
alignment: "center",
},
message: "Reach out to our team.",
ctaLabel: "Contact us",
ctaUrl: "/contact",
},
schema: {
title: {
type: "title",
label: "Title",
default_value: {
text: "Need help?",
level: "h2",
size: "text-3xl",
weight: "font-bold",
alignment: "center",
},
},
message: { type: "textarea", label: "Message", default_value: "" },
ctaLabel: {
type: "text",
label: "Button label",
default_value: "Contact us",
},
ctaUrl: {
type: "link",
label: "Button link",
default_value: "/contact",
attributes: { placeholder: "https://…" },
},
},
} satisfies CmsSectionMetadata;

Each key in the schema selects an editor control for the properties sidebar; see Section field types for the full list. Every schema key maps to a defaultProps key and to a prop your component receives.

The metadata name is only the label shown in the picker — free text, unrelated to the section type (which you set when registering the component). The metadata is attached to its component by file path (the *.CompositionMetadata.ts sibling sitting next to it), never by name, so the two never need to match.

You don't wire any of this up by hand: CmsSectionMetadata is exported by @front-commerce/cms (which owns the CmsSection scope, so you needn't augment CompositionMetadataMap yourself), and the build collects every *.CompositionMetadata.ts sibling automatically.

Render the section

The component is the part a visitor sees. It receives the section's props — the values of the fields you declared in the schema — and renders them:

app/cms/sections/Callout/Callout.tsx
type Heading = {
text: string;
level?: "h1" | "h2" | "h3" | "h4" | "h5" | "h6";
};

export interface CalloutProps {
title?: Heading;
message?: string;
ctaLabel?: string;
ctaUrl?: string;
}

export default function Callout({
title,
message,
ctaLabel,
ctaUrl,
}: CalloutProps) {
if (!title?.text) return null;
const HeadingTag = title.level ?? "h2";

return (
<div className="callout">
<HeadingTag>{title.text}</HeadingTag>
{message ? <p>{message}</p> : null}
{ctaLabel && ctaUrl ? (
<a className="callout__cta" href={ctaUrl}>
{ctaLabel}
</a>
) : null}
</div>
);
}

You only render your own props. The renderer wraps each section and applies its common options for you — the section-level config (background, spacing, visibility) and style become the wrapper's inline styles, so you never handle them. It also passes _sectionId, _index, and _config to your component if you ever need them.

Keep the component lean and reuse your theme's own components — headings, buttons, and so on — so your sections stay visually consistent with the rest of the storefront. The complete example does exactly that.

Register the section

Finally, register the component in the CmsSection content composition scope. This links the section type to your component — the same mechanism the built-in sections use — so the page renderer can resolve stored sections to it. Declare the composition, then register it from your extension's onContentCompositionInit hook:

app/cms/sections/index.ts
import { createContentComposition } from "@front-commerce/core";

// One composition holds every section your app registers — add an entry per
// section.
export const appSections = createContentComposition("CmsSection", [
{
// the entry `name` is the section `type`: a stable, unique id persisted
// backend-side and used to resolve this component. It is unrelated to the
// metadata `name` (which is only the picker label).
name: "CalloutSection",
client: {
component: new URL("./Callout/Callout.tsx", import.meta.url),
fragment: null,
},
},
]);
your extension's index.ts
import { defineRemixExtension } from "@front-commerce/remix";
import { appSections } from "./app/cms/sections";

export default function customCmsSections() {
return defineRemixExtension({
meta: import.meta,
name: "custom-cms-sections",
unstable_lifecycleHooks: {
onContentCompositionInit(composition) {
composition.registerComposition(appSections);
},
},
});
}

Now SectionRenderer resolves every section of that type to your component through CompositionComponent<"CmsSection">, so it renders wherever the type appears on a page. And because its metadata sibling is collected automatically, the section also shows up in the editor's "Add a section" picker (grouped under its category), with its schema driving the properties sidebar — no theme override required.

note

A component registered without a *.CompositionMetadata.ts sibling still renders wherever a page references its type, but it won't appear in the picker — it has no label, icon, or schema to show there.

Complete example

The cms-section-demo example extension (under skeleton/example-extensions/) is a complete, runnable reference: it registers a Callout section, ships its Callout.CompositionMetadata.ts sibling, and renders the section component — everything described above, wired together.