Search documentation

Search documentation

Visual Builder

Components

Build accessible React components for Contoprix blocks.

A component is a CMS-to-React contract#

A Visual Builder component has two halves:

  1. A Component Type in Contoprix, which defines the editor fields and stable code.
  2. A React component in your application, which receives a block and turns the values into accessible UI.

The code joins them. A Component Type code of hero_banner must have a registry entry for hero_banner.

Component props#

Every registered component receives ContoprixComponentProps:

PropPresent forMeaning
settingsComponent blockThe component field values.
contentContent-single blockOne resolved content entry.
contentsContent-list blockResolved content entries.
formForm blockA lightweight form code/name reference.
previewAttributesPreview with visual editingAttributes to spread on the visible outer element.

Custom content values are always inside entry.data.

Example: component settings#

Create a Component Type with code call_to_action and fields heading, description, button_label, and button_url.

src/components/contoprix/CallToAction.tsx
import type { ContoprixComponentProps } from "@contoprix/react";

type CallToActionSettings = {
  heading?: string;
  description?: string;
  button_label?: string;
  button_url?: string;
};

export default function CallToAction({
  settings,
  previewAttributes,
}: ContoprixComponentProps) {
  const cta = settings as CallToActionSettings | undefined;
  if (!cta?.heading) return null;

  const hasButton = Boolean(cta.button_label && cta.button_url);

  return (
    <section {...previewAttributes}>
      <h2>{cta.heading}</h2>
      {cta.description ? <p>{cta.description}</p> : null}
      {hasButton ? <a href={cta.button_url}>{cta.button_label}</a> : null}
    </section>
  );
}

Register it:

src/contoprix/components.ts
import type { ComponentRegistry } from "@contoprix/react";
import CallToAction from "@/components/contoprix/CallToAction";

const components: ComponentRegistry = {
  call_to_action: CallToAction,
};

export default components;

Example: a content-list block#

A content-list block gives the component an array of entries. Read model fields from each entry's data object.

src/components/contoprix/ArticleList.tsx
import type { ContoprixComponentProps } from "@contoprix/react";
import type { ContoprixContentEntry } from "@contoprix/types";

type ArticleData = {
  title?: string;
  summary?: string;
};

export default function ArticleList({
  contents,
  previewAttributes,
}: ContoprixComponentProps) {
  const entries = (contents as ContoprixContentEntry[] | undefined) ?? [];

  if (entries.length === 0) return null;

  return (
    <section {...previewAttributes}>
      <ul>
        {entries.map((entry) => {
          const article = entry.data as ArticleData;

          return (
            <li key={entry.id}>
              <a href={"/blog/" + entry.slug}>{article.title ?? "Untitled article"}</a>
              {article.summary ? <p>{article.summary}</p> : null}
            </li>
          );
        })}
      </ul>
    </section>
  );
}

If the content model does not guarantee a slug, use a fallback or omit the link. Never assume a model field is present just because one entry happened to have it.

Forms need a separate fetch#

A form block only contains a form reference. It intentionally does not contain a live form schema or submission token because a page response can be cached.

For a form renderer:

  1. Receive form.code from the block.
  2. Fetch the current form schema on the server or through a protected same-origin route.
  3. Submit through a server route that keeps delivery credentials private.
  4. Handle validation and submit errors in the UI.

Do not put a live submission token in a cached page payload.

Custom renderer versus generic fallback#

BlockRenderer resolves a custom registry component first. If none exists, it can generically render an unregistered component block when you provide a matching pulled schema.

Forms and content-entry blocks need intentional custom rendering when the generic fallback is not suitable. This is normal: their data and user experience depend on your application.

Make components easy for editors and visitors#

  • Use clear field labels and descriptions in the Component Type.
  • Mark required fields only when the component cannot render without them.
  • Hide optional UI instead of outputting empty paragraphs or buttons.
  • Validate URLs before using them as links.
  • Render meaningful image alt text.
  • Use semantic landmarks and headings.
  • Support empty, partial, and unexpected data states.
  • Spread previewAttributes onto the visible outer wrapper.
  • Test the component in draft preview and published delivery.

Update components after schema changes#

Use the CLI as a safe starting point after creating or changing Component Types:

Terminal
npx contoprix pull
npx contoprix generate
npx contoprix components
npx contoprix validate

The generated files are not your finished design. Use them to confirm registry codes and field shapes, then refine the UI with your application's design system.