Dropdown variants in the toolbar

--- status: draft ---


status: draft

The settings sidebar ships two dropdown primitives plus the standalone VarPicker for design-token selection. This doc explains which is which, what triggers them, and lists every registered property by primitive.

TLDR

  • UnifiedDropdown — the multi-column popover (Auto / Full / Screen / Min / Max / Fit | S / M / L / XL | 2ths / 3ths / …). Lives inside UniversalInput. Used only for property defs whose input.type === "universal" (plus a few hand-rolled inputs that wrap UniversalInput directly: WidthInput, HeightInput, GapInput, ShadowInput, EffectsClassInput, ShorthandInput).
  • ToolbarDropdown — the single-column Headless UI Listbox (None / Extra Small / Small / Base / …). Used for every other selectable property: tailwind-select, plain select, plus a bunch of bespoke editors (actions, conditions, modifiers, text-style editor, media toolbar sort, icon set picker, design-system styles tab).
  • VarPicker — the FloatingPanel that opens from the {} braces type-chip. Lists every design token (palette + typography + styleGuide + custom) and lets the user pick / create / edit. NOT a dropdown — a separate full-CRUD surface. See var-picker.md.

ToolbarItem with type="select" always renders ToolbarDropdown. There is no hybrid.

Where they live

PrimitiveFileTrigger
UnifiedDropdownpackages/sdk/src/chrome/toolbar/inputs/universal-input/UnifiedDropdown.tsxLayout config keyed by propTag / tailwindKey in layouts.ts (COMMON_SIZING_LAYOUT, COMMON_SPACING_LAYOUT, COMMON_NAMED_ONLY_LAYOUT, …)
ToolbarDropdownpackages/sdk/src/chrome/toolbar/ToolbarDropdown.tsxHeadless UI <Listbox>. Children are <option> (or <optgroup>) elements; the component flattens them.

Dispatch happens in PropertyRenderer.tsx:64:

case "universal"        → UniversalInput  → UnifiedDropdown
case "tailwind-select"  → TailwindInput / ToolbarItem(type="select") → ToolbarDropdown
case "select"           → ToolbarItem(type="select")                → ToolbarDropdown
case "tailwind-radio"   → ToolbarItem(type="radio")                 → segmented chips (neither)
case "checkbox" / "text" → ToolbarItem                              → not a dropdown
case "shorthand"        → ShorthandInput → UniversalInput → UnifiedDropdown
case "custom"           → component-specific (often ToolbarDropdown)

Properties using UnifiedDropdown (multi-column)

Source: every registered property def with input.type: "universal" in packages/sdk/src/chrome/toolbar/inspector/registry/properties/.

SectionProperty ids
Layout (layout.ts)width, height, maxWidth, maxHeight, minWidth, minHeight
Display (display.ts)display, position, cursor, overflow, zIndex
Appearance (appearance.ts)shadowSize, opacity, ringWidth, ringOffsetWidth, outlineWidth, outlineOffset
Interactions (interactions.ts)hover.borderWidth, hover.scale, hover.shadowSize, hover.opacity, hover.translateY, hover.ringWidth, hover.ringOffsetWidth

Also reaches UnifiedDropdown indirectly via wrapper inputs:

  • WidthInput, HeightInput — main-tab width/height pickers (Layout panel)
  • GapInput — gap / gap-x / gap-y (Alignment panel)
  • ShadowInput — shadow class picker
  • EffectsClassInput — generic class picker for the effects builder
  • ShorthandInput — wraps UniversalInput per side for margin, padding, etc.

Layout configs that drive the column structure (in layouts.ts):

COMMON_SIZING_LAYOUT (3-col: named + numeric + fractions) → width, height, maxWidth, minHeight. This is what screenshot 1 shows.

COMMON_SPACING_LAYOUT (2-col: tokens+named | numeric) → margin, padding, gap, gapX, gapY.

COMMON_NAMED_ONLY_LAYOUT (single named col, stacked) → display, position, flexDirection, alignItems, justifyContent, fontSize (note: fontSize is tailwind-select in the registry, so this layout entry is unused — see "Surprises" below), fontWeight, textAlign, borderStyle, twTransform.

COMMON_NUMERIC_LAYOUT / COMMON_NUMERIC_SUBGROUPED_LAYOUT / COMMON_TIME_LAYOUT / RING_LIKE_WIDTH_LAYOUT / COMMON_NAMED_OTHER_COLUMN / COMMON_NAMED_OTHER_SPLIT_LAYOUT — see layouts.ts for the full keyset.

Properties using ToolbarDropdown (single-column Listbox)

Registered property defs

input.type: "tailwind-select":

SectionProperty ids
AppearanceborderStyle, divideX, divideY, divideStyle, outlineStyle
Displayorder, visibility, pointerEvents, userSelect, float, verticalAlign, fontSmoothing, resize, touchAction, cssAppearance, boxSizing, isolation, columns, breakBefore, breakInside, breakAfter, listStyleType, listStylePosition, tableLayout, captionSide, fill, stroke, strokeWidth, cssContent, srOnly
Interactionshover.borderStyle
LayoutaspectRatio
BackgroundbgClip, bgBlend, mixBlend
AlignmentgridColSpan, gridRowSpan
TypographyfontSize, fontWeight, lineHeight, tracking, indent, transform, textDecoration, decorationStyle, decorationThickness, textWrap, textOverflow, hyphens

input.type: "select":

SectionProperty ids
TypographywhiteSpace, wordBreak
AriaariaExpanded, ariaSelected, role, tabIndex, ariaHidden, ariaLive

Bespoke (non-registry) consumers

Each of these imports ToolbarDropdown directly:

FileWhat it picks
inspector/mainTabs/ConditionalContainerMainTab.tsxconditional-container mode
inputs/modifiers/ModifiersPickerBody.tsx, ExclusiveDropdown.tsxbreakpoint / pseudo modifier
inputs/action/ActionEditorPanel.tsx, ActionInput.tsx, HandlerEditorPanel.tsxaction type, handler event, target page
inputs/advanced/ConditionsInput.tsxcondition type / operator / source
inputs/typography/TextStyleEditorPanel.tsxpreset font-size / weight / line-height / letter-spacing / transform / decoration — this is screenshot 2
inputs/media/components/MediaToolbar.tsxmedia library sort, kind filter
dialogs/IconDialog/components/IconsTab.tsxicon set (tb / fa / si / …)
viewport/design-system/components/StylesTab.tsxdesign-system style tokens

ToolbarItem itself (ToolbarItem.tsx:187) is the entry point that routes type="select" into ToolbarDropdown, so any bespoke main-tab passing <ToolbarItem type="select"> also lands here.

Surprises / inconsistencies

  • fontSize, fontWeight, textAlign have entries in DROPDOWN_LAYOUTS (COMMON_NAMED_ONLY_LAYOUT) but the registry registers them as tailwind-select — so they render through ToolbarDropdown, never through UnifiedDropdown. The layout entries are dead config. Either flip the registry to universal or delete the layout entries.
  • borderStyle is registered as tailwind-select (Appearance) → ToolbarDropdown, while display, position, flexDirection, alignItems, justifyContent are universalUnifiedDropdown. All five are pure named-only enums, so the split is historical, not principled.
  • The two primitives use different "empty" affordances — UnifiedDropdown has a sticky Reset to Default footer; ToolbarDropdown represents empty as a __ph_empty__ sentinel labeled None. Be aware when porting between them.

Picking which one to use

  • New tailwind class prop where the value scale benefits from columns (numeric scale + named keywords + fractions, hint pixels/percent) → universal + add a DROPDOWN_LAYOUTS entry → UnifiedDropdown.
  • New tailwind class prop that is a flat enum (one short list of keywords) → tailwind-selectToolbarDropdown. Don't add a layout entry.
  • New non-class string field (aria role, action type, etc.) → select with explicit options[]ToolbarDropdown.

Related docs