Skip to main content
Urbicon UI

Select

A dropdown for choosing from a list of options, wired for forms.

Playground

Tier Style variant
<script lang="ts">
  import { Select } from '@urbicon-ui/blocks';

  const options = [
    { label: 'Svelte', value: 'svelte' },
    { label: 'React', value: 'react' },
    { label: 'Vue', value: 'vue' },
    { label: 'Angular', value: 'angular' }
  ];
</script>

<Select
  {options}
  clearable
  label="Framework"
  placeholder="Choose a framework"
/>

01 Purpose

Select shows the chosen value and hides the full list until the user opens it.

Reach forWhen
Select (this)One choice from a moderate list, where a closed control keeps the form compact.
RadioGroupA short set of two to five options, all worth showing at once.
ComboboxA long list the user needs to filter by typing.
MenuFiring actions or commands, not binding a form value.

02 Examples

Bound value

bind:value holds the value of the picked option and is null until something is picked, so country is a $state<string | null>(null) in the parent. clearable swaps the chevron for a reset control once there is something to reset.

Selected: null

<Select
  label="Country"
  bind:value={country}
  clearable
  placeholder="Choose a country"
  options={[
    { label: 'Germany', value: 'de' },
    { label: 'France', value: 'fr' },
    { label: 'Spain', value: 'es' },
    { label: 'Italy', value: 'it' },
    { label: 'United Kingdom', value: 'uk' }
  ]}
/>
<p class="text-text-tertiary text-xs">
  Selected: <code class="text-text-primary">{country ?? 'null'}</code>
</p>

Grouped options

groups takes the place of options and puts each list under a section label. The arrow keys walk the options and pass over the headers. An option with disabled: true stays visible, and the arrows pass over it too.
<Select
  label="Timezone"
  placeholder="Select timezone"
  groups={[
    {
      label: 'Americas',
      options: [
        { label: 'New York (EST)', value: 'est' },
        { label: 'Chicago (CST)', value: 'cst' },
        { label: 'Los Angeles (PST)', value: 'pst', disabled: true }
      ]
    },
    {
      label: 'Europe',
      options: [
        { label: 'London (GMT)', value: 'gmt' },
        { label: 'Berlin (CET)', value: 'cet' },
        { label: 'Moscow (MSK)', value: 'msk' }
      ]
    }
  ]}
/>

Several at once, with counts

multiple binds value to an array. Every row gains a checkbox, the trigger lists the picked labels, and the listbox stays open so the user can tick a few in one go. An option's hint is trailing secondary text — the facet count that says whether the filter is worth applying. It is a string, so the formatting stays yours, and it is part of the row's text, so the option is announced as “Bug 24”.

Picked: none

<Select
  label="Tags"
  multiple
  bind:value={tags}
  placeholder="Select tags"
  options={[
    { label: 'Bug', value: 'bug', hint: '24' },
    { label: 'Documentation', value: 'docs', hint: '8' },
    { label: 'Enhancement', value: 'enhancement', hint: '113' },
    { label: 'Good first issue', value: 'good-first-issue', hint: '3' }
  ]}
/>
<p class="text-text-tertiary text-xs">
  Picked: <code class="text-text-primary">{tags.join(', ') || 'none'}</code>
</p>

In a form

name renders a hidden input carrying the selected value, so a plain form submits the choice without any JavaScript. required adds the asterisk to the label and nothing else: the hidden input carries no native constraint, so check the value on submit and hand the message back as error, which takes the place of the helper text and switches the field to its danger styling.
Used for every invoice on this account

Submitted: nothing yet

<form class="flex flex-col gap-3" onsubmit={handleSubmit}>
  <Select
    label="Currency"
    name="currency"
    required
    helper="Used for every invoice on this account"
    error={currencyError}
    placeholder="Select..."
    options={[
      { label: 'Euro (EUR)', value: 'eur' },
      { label: 'US Dollar (USD)', value: 'usd' },
      { label: 'British Pound (GBP)', value: 'gbp' }
    ]}
  />
  <Button type="submit" size="sm" class="self-start">Save</Button>
</form>
<p class="text-text-tertiary text-xs">
  Submitted: <code class="text-text-primary">{submitted ?? 'nothing yet'}</code>
</p>

03 Customization

Primary-tinted filter

class lands on the wrapper around the whole field, so a trigger restyle goes through slotClasses instead, and the open list is a second slot of its own. Whatever you pass is merged into the tv() defaults, which is why the radius tier, the focus ring and the keyboard behaviour survive it.
<Select
  label="Sort by"
  value="popular"
  options={[
    { label: 'Most popular', value: 'popular' },
    { label: 'Newest first', value: 'newest' },
    { label: 'Price: low to high', value: 'price-asc' },
    { label: 'Price: high to low', value: 'price-desc' }
  ]}
  slotClasses={{
    trigger: 'bg-primary-subtle border-primary text-primary-text hover:border-primary',
    chevron: 'text-primary',
    listbox: 'border-primary'
  }}
/>

If every Select in the app should share this treatment, set it once as a defaults entry for Select on a BlocksProvider. A preset is the opt-in variant of the same thing: it reaches only the controls that name it through their preset prop.

A field that should read as the text it sits in is the bare variant, not a stack of reset classes: no frame, no fill, no padding, no fixed height, and size keeps only its type step. It needs context that says it is a field — a placeholder, a rule under the line, a label before it — and it keeps the one thing such a reset usually loses, a focus outline, whose colour is the --blocks-focus-ring-color custom property. Worked through on the Input page.

This is one of five ways to restyle a block. See Customization for class, slotClasses, unstyled, preset and provider-level overrides.

04 Accessibility

ARIA roles

The trigger is a role="combobox" with aria-expanded and aria-controls pointing at the panel, a role="listbox" of role="option" rows carrying aria-selected. Focus stays on the trigger the whole time and the highlighted row travels with aria-activedescendant. A label is linked through aria-labelledby, and where there is none the trigger falls back to the aria-label you pass it. Helper and error text reach the trigger through aria-describedby, and an error sets aria-invalid.

Keyboard

Enter, Space and both arrow keys open the listbox. / move through the selectable options, wrapping at the ends and passing over disabled rows. Home / End highlight the first and last of them, and Enter / Space select whatever is highlighted. Focus stays on the trigger while the list is open, so Escape only closes it. Tab closes it and moves on to the next tab stop, which is the clear control whenever clearable has something to clear.

05 API Reference

39 props
39 props
Prop
Type
Default
Description

06 Types

Local type definitions used by this component.

13 types
Name
Kind
Category
Used by
Description

07 Installation

Import

import { Select } from '@urbicon-ui/blocks';