`Chip` — the sidebar row primitive

Chip is the ONE primitive used for every "label-on-the-left + control-on-the-right" row in the editor sidebar. There is no ToolbarRowFrame, PopoverChip, BgWrap, Wrap, Labler, or MobileDesktopLabels — those existed historically and have a…

Chip is the ONE primitive used for every "label-on-the-left + control-on-the-right" row in the editor sidebar. There is no ToolbarRowFrame, PopoverChip, BgWrap, Wrap, Labler, or MobileDesktopLabels — those existed historically and have all been deleted. If you find a reference to one of them, replace with Chip.

Source: packages/sdk/src/chrome/primitives/Chip.tsx. Re-exported via @/chrome/primitives.

Mental model

[ label  ⓢ ] [ frame holding the control       ] [ trailing X ]
   ↑    ↑                ↑                              ↑
  truncation    border (or bare),                  optional clear /
  tooltip +     focus ring, h-8                    secondary actions
  click-forward (or min-h-8 with `grow`)
                       ↑
              `swatch` variant drops left
              padding for full-bleed leading

The label gutter (w-20 by default) is baked into Chip. The breakpoint-pill indicator (BreakpointChip) renders next to the label automatically when propKey + propType indicate a class-typed prop. Authors do not render BreakpointChip themselves.

Modes

mode="input" (default)

Use when the row's control is an inline element (text input, segmented control, swatch button, slider, etc.).

<Chip label="Format" frame="bare">
  <ToolbarSegmentedControl … />
</Chip>

<Chip label="Background Color" propKey="background" propType="class">
  <input type="text" className="input-plain" … />
</Chip>

Props:

  • label?: string — gutter label. Omit for label-less rows.
  • onLabelClick?: () => void — click forward (focus / open the control).
  • trailing?: ReactNode — affordance(s) inside the frame, after children (e.g. InlineClearButton).
  • onClick? — click handler attached to the frame.
  • variant?: "default" | "swatch"swatch drops left padding so a full-bleed leading control hits the inner border (color picker swatch).
  • frame?: "bordered" | "bare" — drop the input-wrapper border for controls that own their own chrome (segmented controls, custom buttons, multi-line layouts).
  • grow?: boolean — replace h-8 with min-h-8 (textarea / multi-line content).
  • passthrough?: boolean — escape hatch. Render children directly with no frame, label, or trailing. Used when a parent already provides the row shape.
  • propKey?, propType?, index?, propItemKey? — when set + class-typed, the breakpoint pill renders next to the label.
  • labelWidth?: string — override the default w-20 gutter (rare; only when alignment with neighbouring rows requires it).

mode="popover"

Use when the entire row is a click-to-open trigger for a FloatingPanel. Chip owns the leading icon + summary + session-aware clear ×.

<Chip
  mode="popover"
  ref={triggerRef}
  label={typeLabel}
  open={open}
  onTriggerClick={() => (open ? setOpen(false) : openPanel())}
  onClear={handleClear}
  triggerAriaLabel="Edit action"
  clearAriaLabel="Remove action"
  leading={<Icon className="size-3.5" aria-hidden />}
  summary={summary}
/>

Props (in addition to label / propKey / etc. from ChipCommon):

  • onTriggerClick: () => void — click on the chip body opens the popover.
  • onClear: () => void — click on the trailing × clears the underlying value. Composes with SessionAddedAtom so empty popover-mode rows hide on the next render.
  • triggerAriaLabel: string, clearAriaLabel: string — required.
  • leading?: ReactNode — icon / swatch / color preview to the left of summary.
  • summary?: ReactNode — short description of the current value (e.g. "Link → /shop").
  • variant?: "default" | "preview"preview makes leading take the full chip body (gradient swatch / image preview); previewOverlay renders a small pill on top.
  • trailingExtras?: ReactNode — extras rendered inside the frame's trailing column, BEFORE the ×. Keep them small (size-5 icon-button shape). Used by show-hide chips for the eye-toggle peek.

forwardRef lands on the trigger button so the panel can position next to it.

Patterns

Plain text input

<Chip label="Title" propKey="title" propType="component">
  <input
    type="text"
    className="input-plain flex-1"
    value={value}
    onChange={e => setValue(e.target.value)}
  />
</Chip>

Segmented control (no double border)

Segmented controls own their own track. Use frame="bare".

<Chip label="Direction" frame="bare" propKey="flexDirection" propType="class">
  <ToolbarSegmentedControl … />
</Chip>

Color picker (swatch variant)

variant="swatch" drops the left padding so the swatch fills the rounded-left corner.

<Chip
  label="Background"
  variant="swatch"
  propKey={propKey}
  propType="class"
  trailing={value ? <InlineClearButton onClick={clear} /> : null}
>
  <button className="relative h-full w-full" onClick={openPicker}>
    {/* swatch fill */}
  </button>
</Chip>

Popover trigger

<Chip
  mode="popover"
  ref={triggerRef}
  label="Action"
  open={open}
  onTriggerClick={openPanel}
  onClear={removeAction}
  triggerAriaLabel="Edit action"
  clearAriaLabel="Remove action"
  leading={<TbLink className="size-3.5" />}
  summary={`${type} → ${target}`}
/>
{open && <Suspense fallback={null}><ActionEditorPanel … /></Suspense>}

Empty-state add-row

<LabeledAddChip label="Action" cta="Add..." onClick={openPicker} />

(LabeledAddChip is a thin shell over Chip.)

Don'ts

  • Don't wrap a Chip in a <div className="flex items-center gap-0.5"><span>{label}</span>…</div> outer. The chip owns the row.
  • Don't render BreakpointChip next to a Chip. Pass propKey + propType and the chip places it for you.
  • Don't nest <Chip><Chip>…</Chip></Chip> for a single visual row. Pick the mode/variant/frame combo that produces the row you want.
  • Don't hand-roll input-wrapper h-8 … markup. If the chip can't produce the shape you need, talk to the team — extending Chip beats a parallel primitive.
  • Don't reach for ToolbarRowFrame / PopoverChip / BgWrap / Wrap / Labler / MobileDesktopLabels — they don't exist anymore.