Skip to content
HomePagesHomePages template kit

Add a select slot

Give the user a closed set of choices — a slot whose value is one of a vocabulary you write out or one a bound fact supplies.

Give the user a closed set of choices: a slot whose value is one of a vocabulary you write out, or one the fact it is bound to already supplies.

  • A section to add the slot to — Install scaffolds one.
  • Its fixtures.ts built from a scenario, so the fact behind the slot resolves in preview — Pick a fixture scenario.

A select slot still needs a source binding, exactly like every other slot type — the vocabulary alone only declares the closed set the editor’s dropdown offers. The cleanest no-AI fill for a closed set is usually a fact that already resolves to one of a closed set of strings. Write the vocabulary out as select.of’s first argument and bind the value with source.

Add select and fact to the section’s existing kit import — the scaffolded file takes only defineSchema, text and textBlock, so both are missing until you do.

// section.ts — add a select slot, seeded from a closed-vocabulary fact
property_type: select.of(
["single_family", "condo_townhome", "multi_family", "investment"],
{ label: "Property type", source: fact.property.property_type, writeback: false },
),

An option is a bare string when the stored key and the label are the same thing, or { value, label } when they differ. select.of is the hand-authored vocabulary; select({ source: … }) — no array — instead points the dropdown at the bound fact’s own domain, which any direct fact binding supplies (see the schema declaration).

// Renderer.tsx — supply the label yourself; the stored value is an internal key
const PROPERTY_TYPE_LABEL: Record<string, string> = {
single_family: "Single family",
condo_townhome: "Condo / townhome",
multi_family: "Multi-family",
investment: "Investment",
};
<Slot.Select slot={slot.property_type} as="span">
{PROPERTY_TYPE_LABEL[slot.property_type.value]}
</Slot.Select>

The kit ships no catalog of option labels, so the mapping is yours to supply — as children here, or as a format callback. Supply neither and the stored key renders as its own text, which is legible but rarely what you want on a page. Slot.Select never emits a text leaf, which is the point: the stored value (single_family) is an internal key, and leafing it would let a keystroke in the editor overwrite the key instead of changing which label is shown for it. Slot.Url withholds the leaf for the same reason — see values that never leaf.

slot.property_type.value is string — an unfilled slot arrives as the blank "", and slot.property_type.present is the absence signal. The general source contract still applies to a select slot, but no fill-spec decision type constrains its output to the closed vocabulary — so a source binding onto a fact that already resolves within the set is the reliable way to fill one.

Two additions — one in section.ts, one in Renderer.tsx — plus select and fact on the section’s existing kit import:

// section.ts — in `defineSchema`'s `slots`
property_type: select.of(
["single_family", "condo_townhome", "multi_family", "investment"],
{ label: "Property type", source: fact.property.property_type, writeback: false },
),
// Renderer.tsx — the label map, then the call site wherever the value belongs
const PROPERTY_TYPE_LABEL: Record<string, string> = {
single_family: "Single family",
condo_townhome: "Condo / townhome",
multi_family: "Multi-family",
investment: "Investment",
};
<Slot.Select slot={slot.property_type} as="span">
{PROPERTY_TYPE_LABEL[slot.property_type.value]}
</Slot.Select>

Verify with the author loop.

  • schema-invalid — the slot declares a vocabulary but carries no source binding.
  • missing-slot-marker — the new editable slot never reaches the rendered DOM with its marker.
  • sidebar-order — the slot is declared in one position in the schema and rendered in another.