Group slots in editor
Collapse several slots into one card in the editor's sidebar, and mark the container the editor resolves them through.
Collapse several slots into a single card in the editor’s sidebar, instead of one card per slot.
Prerequisites
Section titled “Prerequisites”- Two or more slots that belong together — Add a slot adds an address, and A select slot adds a property type.
- The markup that renders them, so they can share one container —
Install scaffolds a
Renderer.tsx.
Cards are declared in layout, beside slots. Each group(...) entry is one
card; anything else in layout is a bare row, which stays its own default card.
Add group to the section’s existing kit import — the scaffolded file takes only
defineSchema, text and textBlock, so it is missing until you do.
// section.ts — a `layout` key, beside `slots`layout: [group("listing", "Listing", ["address", "property_type"])],A card declares two independent facts, in the same order a slot does: the id
your renderer reads it back by, then the label the editor shows on the card.
Neither is derived from the other, so pick each on its own merits — an id that
reads well in group.listing, and a title that reads well to whoever is editing.
members is typed to this section’s own slot keys, so a card naming a slot the
schema does not declare is a compile error. A member is a bare key (a full-width
row) or an [a, b] pair (one 2-up row of two short fields).
Then mark the container the members share. Add bindGroups and SlotGroup to the
renderer’s existing kit import — the scaffolded file takes only bindSlots,
Section and Slot, so both are missing until you do. bindGroups gives one
binding per declared card, the mirror of bindSlots and called once beside it, and
SlotGroup spreads that binding’s attribute onto the element you name:
// Renderer.tsx — bind the cards, then wrap the members' shared containerconst group = bindGroups(schema);
<SlotGroup slot={group.listing} as="div" className="flex flex-col gap-2"> <Slot.Text slot={slot.address} as="p" /> <Slot.Select slot={slot.property_type} as="span" /></SlotGroup>The editor resolves which card a click belongs to by walking up from the clicked element to the nearest marked container. That is the entire mechanism, and it is why the container has to be an ancestor rather than a sibling: a member marked outside it resolves to no card at all, while the page still renders and the card still appears. A section that renders a member twice — a desktop tree and a mobile one — marks both containers with the same binding; a mirror that should not be editable drops its marker instead.
Grouping is presentation only. It changes which card the editor draws and nothing else: not the fill, not the values, not the props your renderer receives.
Complete diff
Section titled “Complete diff”Two additions — one in section.ts, one in Renderer.tsx — plus group on the
section’s existing kit import and bindGroups and SlotGroup on the renderer’s:
// section.ts — beside `slots`, at the top level of `defineSchema`layout: [group("listing", "Listing", ["address", "property_type"])],// Renderer.tsx — the binding, then the members' shared containerconst group = bindGroups(schema);
<SlotGroup slot={group.listing} as="div" className="flex flex-col gap-2"> <Slot.Text slot={slot.address} as="p" /> <Slot.Select slot={slot.property_type} as="span" /></SlotGroup>Verify
Section titled “Verify”Verify with the author loop.
Rules that can fire
Section titled “Rules that can fire”group-ancestry— a member is marked on an element that is not a descendant of the element carrying its card’s marker.sidebar-order— the cards appear in the rendered page in an order the schema does not declare.schema-invalid— two cards share an id, or one slot is a member of two cards.
See also
Section titled “See also”- Arranging the editor sidebar with
layout— the declaration’s full contract. SlotGroup/bindGroups— the two primitives that put the card’s marker in the DOM.group-ancestry— the ancestry the marked container owes every member, with a worked before and after.