← Back to all how-tos

Sidebar controls in depth

Create a custom control

Create your own sideEditProp UI when the built-in controls are not enough for the editing experience you want.

Estimated time: 9 minRaw Markdown

In this how-to, you'll learn how to create a custom sidebar control with SideEditPropType.Custom.

This is the escape hatch for cases where the built-in controls are not enough.

Docs reference: Sidebar controls

When a custom control makes sense

Reach for a custom control when editors need a specialized UI such as:

  • a compound spacing picker
  • a map-based location picker
  • a preset picker with thumbnails
  • a custom token selector tied to your design system

Before using a custom control, it is worth checking whether Select, Autocomplete, Image, or Relationship already covers the use case.

The 2 required pieces

A custom side edit prop needs:

  1. a type: types.SideEditPropType.Custom
  2. a component function that renders the editing UI

React Bricks passes the component:

  • value: the current value of the prop
  • onChange: the function you call when your custom control should save a new value
  • isValid: whether the current value passes validation, useful for your custom UI to show an error state

Example

This example creates a tiny preset picker for section spacing:

import clsx from 'clsx'
import { Text, types } from 'react-bricks/rsc'
 
type SpacingPreset = 'compact' | 'comfortable' | 'spacious'
 
interface SectionBlockProps {
  title: types.TextValue
  spacing: SpacingPreset
}
 
const spacingClasses: Record<SpacingPreset, string> = {
  compact: 'py-6',
  comfortable: 'py-12',
  spacious: 'py-20',
}
 
const SpacingControl = ({
  value,
  onChange,
  isValid,
}: {
  value?: SpacingPreset
  onChange: (value: SpacingPreset) => void
  isValid: boolean
}) => {
  const options: SpacingPreset[] = ['compact', 'comfortable', 'spacious']
 
  return (
    <div className={clsx('grid grid-cols-3 gap-2', !isValid && 'rounded border border-red-500 p-2')}>
      {options.map((option) => (
        <button
          key={option}
          type="button"
          onClick={() => onChange(option)}
          className={clsx(
            'rounded border px-2 py-2 text-xs',
            value === option ? 'border-sky-500 bg-sky-50' : 'border-slate-200'
          )}
        >
          {option}
        </button>
      ))}
    </div>
  )
}
 
const SectionBlock: types.Brick<SectionBlockProps> = ({ title, spacing }) => {
  return (
    <section className={spacingClasses[spacing]}>
      <Text
        propName="title"
        value={title}
        placeholder="Section title..."
        renderBlock={({ children }) => <h2>{children}</h2>}
      />
    </section>
  )
}
 
SectionBlock.schema = {
  name: 'section-block',
  label: 'Section Block',
  getDefaultProps: () => ({
    title: 'A section with spacing presets',
    spacing: 'comfortable',
  }),
  sideEditProps: [
    {
      name: 'spacing',
      label: 'Spacing',
      type: types.SideEditPropType.Custom,
      component: SpacingControl,
      validate: (value) =>
        ['compact', 'comfortable', 'spacious'].includes(value) ||
        'Choose a spacing preset',
    },
  ],
}

Keep the value simple

Even when the UI is custom, the stored value should usually stay simple.

Good examples:

  • a string preset
  • a small object
  • a number

That makes the brick easier to render, validate, and migrate later.

Use custom controls sparingly

Custom controls are powerful, but they also create a larger maintenance surface.

Try to keep them:

  • focused on one editing task
  • visually clear for editors
  • consistent with your design system

If the control becomes too large or too application-specific, it may be a sign that the value belongs somewhere else in the app workflow.

Summary

Custom controls are the advanced extension point for sideEditProps.

Use them when you need a better editing experience than the built-in controls can offer, but still want the value to flow through normal brick props.