SvDropDownList

A single-select dropdown: a trigger button plus a portalled option list, with no typing. Pick with the mouse or the keyboard (type-ahead included); the panel escapes any scroll container so it is never clipped.

SvDropDownList is the classic <select> replacement, restyled to the grid's --sg-* tokens and given a real roving-focus keyboard model. It is controlled via value + onChange, groups options by their group heading, and can virtualize long lists. Because the panel is portalled to <body> and positioned with auto-flip, it stays visible even inside a scrolling toolbar or a dialog.

Related: SvComboBox · SvListBox · Selection overview

Installation

Add it with the CLI - this drops a ready-to-edit SvDropDownList starter into your app:

Prefer to see it first? npx @svgrid/ui try drop-down-list opens it in a throwaway sandbox - no project needed.

Or install the package and import it directly. SvDropDownList ships free in @svgrid/grid (dependency-free):

import { SvDropDownList } from '@svgrid/grid'

Example

Open the live example: Drop-down list (Selection)

<script lang="ts">
  import { SvDropDownList, type ListOption } from '@svgrid/grid'
  const options: ListOption[] = [
    { value: 'open', label: 'Open' },
    { value: 'wip', label: 'In progress' },
    { value: 'done', label: 'Done' },
  ]
  let value = $state<string | null>(null)
</script>

<SvDropDownList label="Status" {options} {value} onChange={(v) => (value = v)} />

Props

Prop Type Default Description
options ReadonlyArray<ListOption> - The options. A group heading buckets them into sections.
value string | number | null null The selected value.
onChange (value: string | number) => void - Fires with the newly picked value.
placeholder string 'Select…' Shown on the trigger when nothing is selected.
autoOpen boolean false Focus the trigger and open the panel on mount.
virtual boolean false Window the option list for large sets. Flat lists only.
rowHeight number 34 Fixed option height in px; must match the CSS.
maxRows number 8 Options shown before the panel scrolls (native-select behaviour).
resizable boolean false Show a bottom drag grip so users can resize the open panel.
readonly boolean false Value shown; the panel will not open.
item Snippet<[ListOption]> - Custom render for each option.
header / footer Snippet - Content pinned above / below the option list.
noData Snippet - Shown when there are no options.
size sm | md | lg md Control height and font size.
disabled boolean false Blocks interaction and dims the trigger.
label string - Visible field label, wired to the control.
hint string - Helper text under the control.
error string - Error message; announced and styled when set.
required boolean false Marks the field required.
invalid boolean false Applies the invalid state.
name string - Emits a hidden input carrying the value for form posts.
dir ltr | rtl | auto auto Text direction.
ariaLabel string - Accessible name when there is no visible label.
id string - Root id; label/hint/error ids derive from it.

Options use the shared ListOption shape.

Examples

Grouped options

Give options a group and the panel renders labeled sections automatically:

<SvDropDownList {options} label="Priority" />
<!-- options: { value, label, group: 'Urgent' | 'Normal' } -->

Color swatches

Give an option a color (any CSS color) and it renders a small swatch before the label - in the panel and on the selected trigger. Handy when the value is a color, like a status, label, or category:

<SvDropDownList {options} label="Role" />
<!-- options: { value: 'Barista', label: 'Barista', color: '#4f46e5' } -->

Required in a form

Set required and drive invalid / error from your validation to surface a message under the control:

<SvDropDownList label="Country" {options} required
  invalid={submitted && !value} error={!value ? 'Pick one' : undefined} />

Virtualized long lists

For hundreds of flat options, add virtual so only the visible rows render:

<SvDropDownList {options} virtual rowHeight={34} />

Note: a fast scrollbar-thumb drag can outrun windowing for a frame; the panel fills the off-screen area with faint placeholder rows (a skeleton) instead of flashing blank, so it stays visually filled at any scroll speed.

Bounds detection (auto flip up/down)

The panel measures the room around its trigger every time it opens and on scroll/resize. When there is not enough space below and there is more above, it flips up; otherwise it opens down. Either way its height is capped to the space that actually fits the viewport, so a long list near a screen edge scrolls inside the panel instead of overflowing or clipping off-screen. This is automatic - no configuration - and applies to every SvGrid dropdown (SvComboBox, SvAutoComplete, SvMultiSelect, SvGridSelect, SvTreeSelect, and the in-grid cell-editor dropdown), since they share one anchoring engine.

Resizable panel

Add resizable to give the open panel a bottom drag grip (a "····" handle). The user drags it to make the list taller or shorter; the height is clamped so it never grows past the viewport. The grip appears only while the panel opens downward - on an upward flip there is no room below to grow into, so it is hidden.

<SvDropDownList {options} resizable />

The same resizable prop works on SvComboBox and SvAutoComplete:

Open the live example: Resizable dropdowns (Selection)

Cascading selects

Feed the second dropdown from the first, and reset the child on every parent change so it can never keep a value from the old branch:

<script lang="ts">
  import { SvDropDownList, type ListOption } from '@svgrid/grid'
  const countries: ListOption[] = [
    { value: 'us', label: 'United States' },
    { value: 'ca', label: 'Canada' },
  ]
  const regionsByCountry: Record<string, ListOption[]> = {
    us: [{ value: 'ca', label: 'California' }, { value: 'ny', label: 'New York' }],
    ca: [{ value: 'on', label: 'Ontario' }, { value: 'bc', label: 'British Columbia' }],
  }
  let country = $state<string | null>(null)
  let region = $state<string | null>(null)
  const regions = $derived(country ? regionsByCountry[country] ?? [] : [])
</script>

<SvDropDownList label="Country" options={countries} value={country}
  onChange={(v) => { country = String(v); region = null }} />
<SvDropDownList label="Region" options={regions} value={region}
  disabled={!country} onChange={(v) => (region = String(v))} />

Tip: autoOpen focuses the trigger and opens the panel on mount - handy when the dropdown is the first thing a user reaches for in a filter toolbar.

Accessibility

More examples

createDropdownList drives SvDropDownList and a custom trigger+menu render, sharing one value.

Open the live example: Dropdown list - headless (Headless Editors)

Sizes

Every control takes the same three sizes, so a dense toolbar and a roomy form can share components.

<SvDropDownList options={countries} size="sm" />
<SvDropDownList options={countries} size="md" />
<SvDropDownList options={countries} size="lg" />

In a form

The shared field props behave the same on every editor: label names it, hint explains it, and error plus invalid mark it - which is why a validated form does not need per-component handling.

<SvDropDownList
  options={countries}
  label="Label"
  hint="A short hint"
  required
/>

<SvDropDownList
  options={countries}
  label="Label"
  error="Something is wrong"
  invalid
/>

See also

Live examples

  • Drop-down list - SvDropDownList: a status / priority toolbar with grouped options, type-ahead and animated portalled panels driving a live issue list.
  • Resizable dropdowns - Built-in browser bounds detection - every picker panel (SvDropDownList / SvComboBox / SvAutoComplete) opens downward when there is room and flips upward automatically near the window edge. Plus a `resizable` prop that adds a bottom drag grip to the open panel (drag to grow/shrink; height sticks for the session; grip hides on an upward flip). A switch compares fixed-height vs resizable. Theme-aware (light/dark).
  • Dropdown list - headless - createDropdownList drives SvDropDownList and a custom trigger+menu render, sharing one value.