Contents

Mount Your Own Components

Register a React component from your app on RuleCMSWidget so a Custom slot in the composer can render it by name — no RuleCMS redeploy.

What this is for

You already have a React component in your application — a booking form, a pricing calculator, a seat picker. A marketer needs to place it on a RuleCMS page, hide it on phones, and change a variant or a count without you republishing the site every time.

mounts is that handshake. You register the component on RuleCMSWidget under a short name. They drop Custom on the canvas and type that same name. The published page looks the name up in your map and renders your component. The name is a map key, not a string that becomes code.

You need three things: @rulecms/widget-react 22.16.0 or later, @rulecms/source-components-react 20.19.0 or later, and a name you will actually tell a human to type. Write the name down for them, with the props list. The composer has no autocomplete for this.

When to use this — and when not to

You wantUse
A component that already lives in your app, configured by a marketer, no RuleCMS redeployCustom + mounts (this page)
A click handler or other behaviour on one instance, including on a mounted componentCustom Props and Click Handlers (componentProps, keyed by column id)
First-class palette cards, composer preview, and your own attribute editorsComponent Libraries — a published package and a RuleCMS enablement
YouTube, Vimeo, Google Maps, or CalendlyEmbed — not a host component
Hide the slot on phonesColumn Hide — not a Custom setting

The handshake

Custom does not work unless you and the marketer agree on a string. RuleCMS never sees your component, so the composer cannot preview it and cannot offer a list of names. A typo on either side is a dashed box on the live page.

  1. You write the component in your app.
  2. You register it on every RuleCMSWidget / RuleCMSWidgetServer that should be able to render it.
  3. You give the marketer a card: the exact name, the props it reads, and the type of each prop (text, number, yes/no, JSON).
  4. They drop Custom, type the name, fill the props, and publish.
  5. You deploy the app that contains the registration. Publishing the widget is not enough if production is still on a build that does not pass mounts.

Step 1 — Write a component that accepts authored props

Treat the props list as a public configuration surface. Keep internal names and secrets out of it. Event handlers do not come from the composer — pass those with componentProps.

type BookingFormProps = {
  variant?: 'full' | 'compact';
  seats?: number;
  showWaitlist?: boolean;
  theme?: { accent?: string };
  onBooked?: (id: string) => void;
};

export function BookingForm({
  variant = 'full',
  seats = 8,
  showWaitlist = false,
  theme,
  onBooked,
}: BookingFormProps) {
  // your existing form — talks to your API, uses your auth, your styles
  return (
    <form data-variant={variant} style={{ ['--accent' as string]: theme?.accent }}>
      {/* … */}
    </form>
  );
}

Unknown props are ignored by React. A marketer who adds a name you do not read will not break the page. A marketer who picks the wrong type will not break it either — RuleCMS drops a row that fails its declared type rather than guessing.

Step 2 — Register it on the widget

'use client';

import { RuleCMSWidgetProvider, RuleCMSWidget } from '@rulecms/widget-react';
import * as sourceComponents from '@rulecms/source-components-react';
import { BookingForm } from './BookingForm';

export function ClassPage() {
  return (
    <RuleCMSWidgetProvider
      token={process.env.NEXT_PUBLIC_RULECMS_TOKEN}
      libraries={{ default: sourceComponents }}
    >
      <RuleCMSWidget
        publishedKey="…---widget-…"
        mounts={{ 'booking-form': BookingForm }}
      />
    </RuleCMSWidgetProvider>
  );
}

Register every name the marketer might type, including per-device variants:

mounts={{
  'booking-form': BookingForm,
  'booking-form-compact': CompactBookingForm,
}}

The same map works on RuleCMSWidgetServer. Unlike componentProps, this is a component the renderer invokes, so it does not have to cross the Server Component boundary as a function prop of the visitor's. If your component needs hooks or click handlers, mark it 'use client' and register that module.

Step 3 — Give the marketer a card they can follow

They have no schema and no autocomplete. A short written card prevents the dashed-box support ticket.

Component Name: booking-form
(optional on phones: booking-form-compact)

Props
  variant        Text      "full" or "compact"
  seats          Number    how many seats to show
  showWaitlist   Yes / no  true or false, exactly
  theme          JSON      {"accent":"#0af"}

Do not put secrets in Props. They ship with the page.
Hide the slot on phones with Hide on the column, not with a prop.

Point them at Custom Component for the editor side.

What the component receives

On a hit, r-mount renders a wrapper <div> that carries the column's box styles, then your component:

<div style={columnStyles} className={columnClassName}>
  <BookingForm
    {...widgetInstanceProps}
    variant="compact"
    seats={12}
    showWaitlist={true}
    theme={{ accent: '#0af' }}
  />
</div>

Author props are never spread onto the wrapper. Your component is a child, not the box. Size, padding, and hide belong to the column.

widgetInstanceProps (published key, environment, and the rest of the instance bag) are passed first. Author props overwrite on collision. Do not publish a configuration name that clashes with an instance field you still need.

Prop types the composer can author

Composer typeStored asYour component receivesDropped when
Textstringthe string as typednever, if the row is well-formed
Numbernumbera finite numberthe value is not a finite number (twelve, blank)
Yes / nobooleantrue or falseanything other than the exact strings true / false
JSONjsonthe parsed value (object, array, number, …)JSON.parse throws

Keys that are never passed

RuleCMS drops these from the authored list, in the editor and at render. They cannot become behaviour.

  • Event names: anything matching onClick-style onX, plus a set of HTML events such as onclick and onload
  • React reserved: key, ref, children
  • HTML injection: dangerouslySetInnerHTML
  • Prototype chain: __proto__, constructor, prototype

To pass onBooked or onClick, use componentProps addressed at the Custom component's column id:

<RuleCMSWidget
  publishedKey={publishedKey}
  mounts={{ 'booking-form': BookingForm }}
  componentProps={{
    [BOOKING_COLUMN_ID]: { onBooked: openConfirmation },
  }}
/>

componentProps still cannot go on RuleCMSWidgetServer. Fetch on the server and render from a client component, the same pattern as the Custom Props guide.

The name grammar

The composer and the renderer accept the same pattern: start with a letter, then letters, digits, hyphens, or underscores, up to 64 characters.

booking-form          ok
booking_form          ok
BookingForm           ok
bookingForm           ok — and different from booking-form
2fa-form              refused (leading digit)
booking form          refused (space)
booking.form          refused (dot)

Pick one style and keep it. Hyphenated lowercase is the easiest to read over a Slack message and the hardest to camelCase by accident.

What you will see, and what they will see

SurfaceWhat renders
Composer canvasAlways the dashed placeholder. RuleCMS never receives mounts.
Your app, name matches a keyYour component, inside the column box.
Your app, name missing or misspelledThe placeholder, showing the name they typed. Outside production, the console warns once and lists the keys you registered.
Your app, you passed no mountsThe placeholder. Publishing does not invent a registration.
HTML script embedAlways the placeholder. The IIFE does not contain your components and has no way to receive a ComponentType.
r-mount: no host component registered as "booking-form". Registered names: pricing-calc.

Server-rendered pages

Pass the same map to RuleCMSWidgetServer. Your component is rendered on the server if it can be.

import { RuleCMSWidgetServer } from '@rulecms/widget-react/server';
import * as sourceComponents from '@rulecms/source-components-react';
import { BookingForm } from './BookingForm';

export default async function Page() {
  return (
    <RuleCMSWidgetServer
      publishedKey={publishedKey}
      token={token}
      libraries={{ default: sourceComponents }}
      mounts={{ 'booking-form': BookingForm }}
    />
  );
}
The HTML custom-element embed cannot do this. Use @rulecms/widget-react in a React (or Next.js) host. A WordPress page that only loads the script tag will keep showing the dashed box.

Use cases, worked

A booking form the marketer can turn on and off

Register booking-form. They drop Custom, type that name, and use column Hide to take it off phones or off the page during a studio closure. No prop required if your defaults are fine. To let them pick a class series without a deploy, add a text prop they type, for example series.

A calculator whose numbers they own

Register pricing-calc. They set basePrice (number), currency (text), and showDeposit (yes/no). You keep tax logic and the checkout call in the component. When the price changes, they edit the widget and publish. You do not.

A compact variant on phones

Register both names. They uncheck "Same value for all resolutions" on Component Name and type booking-form-compact for phone, booking-form for desktop. Two components, one slot.

A handler the composer must not author

They configure variant in Props. You pass onBooked with componentProps at that column's id. The marketer never types a function. The live page still opens your drawer.

Things worth knowing

QuestionAnswer
Can the composer preview my component?No. The dashed box is the product. RuleCMS does not have your bundle.
Is the name loaded as code?No. It is mounts[name]. A miss is a placeholder. There is no import(name) and no URL in the widget JSON.
Do I have to register the map on every widget on the page?On every RuleCMSWidget that should be able to resolve those names. A widget you did not pass the map to cannot fill the slot.
What if they type a prop I do not read?Nothing happens. Same as an unknown HTML attribute on a DOM node you do not look at.
Can they pass children?No. Custom is a leaf. Nested cards inside a host component are not shipped.
Are prop values secret?No. They are in the published widget JSON. Anyone can read them. Do not put tokens or passwords there.

Related docs