Phoundry UI

Select

Custom dropdown select with keyboard navigation, option groups, icons, descriptions, clearable state, and validation.

import { Select } from 'phoundry-ui';

Control

Props that affect the trigger button appearance and behavior.

Basic

Selected: apple

Show code
const options = [
  { value: 'apple', label: 'Apple' },
  { value: 'banana', label: 'Banana' },
  { value: 'cherry', label: 'Cherry' },
];

<Select options={options} value={selected} onchange={(v) => selected = v} />

Variants

outline
ghost (minimal)
filled (default)
Show code
<Select options={opts} value={v} onchange={set} variant="outline" />
<Select options={opts} value={v} onchange={set} variant="ghost" />
<Select options={opts} value={v} onchange={set} variant="filled" />

Sizes

sm
md (default)
Show code
<Select options={opts} value={v} onchange={set} size="sm" />
<Select options={opts} value={v} onchange={set} size="md" />

Shrink

Fits the trigger width to its content instead of stretching full-width.

Show code
<Select options={opts} value={v} onchange={set} shrink />

Clearable

Show code
<Select options={opts} value={v} onchange={set} clearable />

Disabled

Show code
<Select options={opts} value={v} onchange={set} disabled />

Validation

Selection required

Show code
<Select options={opts} value={v} onchange={set} invalid error="Selection required" />

Selected indicator

selectedIndicator="check" shows a checkmark instead of tinting the active row (helpful when accent-on-background is too strong).

Show code
<Select
  options={opts}
  value={v}
  onchange={set}
  selectedIndicator="check"
/>

Dropdown

Options support icons, descriptions, and groups for richer dropdowns.

With Icons

Selected: none

Show code
const options = [
  { value: 'svelte', label: 'Svelte', icon: 'logos:svelte-icon' },
  { value: 'react', label: 'React', icon: 'logos:react' },
  { value: 'vue', label: 'Vue', icon: 'logos:vue' },
];

<Select options={options} value={v} onchange={set} />

With Descriptions

Selected: none

Show code
const options = [
  { value: 'sm', label: 'Small', description: '640px max width' },
  { value: 'md', label: 'Medium', description: '768px max width' },
  { value: 'lg', label: 'Large', description: '1024px max width' },
];

<Select options={options} value={v} onchange={set} />

Grouped Options

Selected: none

Show code
const options = [
  {
    label: 'Frontend',
    options: [
      { value: 'svelte', label: 'Svelte' },
      { value: 'react', label: 'React' },
    ],
  },
  {
    label: 'Backend',
    options: [
      { value: 'rust', label: 'Rust' },
      { value: 'go', label: 'Go' },
    ],
  },
];

<Select options={options} value={v} onchange={set} />

Custom Snippet

Pass a custom object with a snippet for fully custom rendering in both the dropdown and the trigger. Set selectable: false to embed interactive content that doesn't trigger selection.

Selected: none

Show code
import { Badge } from 'phoundry-ui';

{#snippet statusRow({ option, selected })}
  <span class="flex items-center gap-2 flex-1 min-w-0">
    <Badge variant={variants[option.value]} dot />
    <span class="text-xs truncate">{option.label}</span>
  </span>
{/snippet}

{#snippet customInput()}
  <TextInput
    value={customStatusText}
    oninput={(v) => customStatusText = v}
    placeholder="Custom status..."
    size="sm"
    variant="fill"
  />
{/snippet}

const options = [
  { value: 'online', label: 'Online', custom: { snippet: statusRow } },
  { value: 'away', label: 'Away', custom: { snippet: statusRow } },
  { value: 'busy', label: 'Busy', custom: { snippet: statusRow } },
  // Non-selectable row with interactive content
  { value: '', label: '', custom: { snippet: customInput, selectable: false } },
];

<Select options={options} value={v} onchange={set} />

Props

PropTypeDefaultDescription
options requiredOptionOrGroup[]Flat options or option groups to display
value requiredT | undefinedCurrently selected value
onchange required(value: T | undefined) => voidCalled when selection changes
placeholder string'Select...'Placeholder text when no value is selected
size 'sm' | 'md''md'Trigger height aligned to Button `sm` / `md` control scale
disabled booleanfalseDisables the select
variant 'outline' | 'ghost' | 'filled''filled'Visual style of the trigger (`ghost` is the minimal inline style).
shrink booleanfalseWhen true, trigger width fits content instead of full width
clearable booleanfalseShows a clear button when a value is selected
required booleanfalseReserved for HTML semantics — not wired to DOM attributes yet; use `invalid` / `error` for validation UX.
invalid booleanfalseApplies error styling to the trigger
error stringError message displayed below the select
id stringHTML id attribute
selectedIndicator 'check' | 'background''background'How the selected option is indicated in the dropdown
name stringHidden input name for form submission
element HTMLButtonElementBindable reference to the trigger button

SelectOption

Props

PropTypeDefaultDescription
value requiredTThe value associated with this option
label requiredstringDisplay text for the option (also used as accessible fallback)
description stringSecondary text shown below the label
icon stringIconify icon string shown before the label
custom SelectOptionCustomCustom rendering config (see below)

SelectOptionCustom

Props

PropTypeDefaultDescription
snippet requiredSnippet<[{ option, selected }]>Svelte snippet that replaces the default option content
selectable booleantrueWhen false, the option row is non-selectable (renders as a div for interactive content)

SelectOptionGroup

Props

PropTypeDefaultDescription
label requiredstringGroup header text
options requiredSelectOption[]Options within this group

Usage tips

  • Use variant="ghost" when the select is inline within a toolbar or header.
  • Pass shrink to prevent the trigger from stretching to full width.
  • Combine invalid + error for form validation; the error renders below the trigger.
  • The dropdown renders as a portal-based overlay for correct z-index stacking.
  • Full keyboard navigation: Arrow keys, Enter/Space to select, Escape to close.
  • Pass name to emit a hidden input for plain HTML forms alongside the visible button trigger.