Select
A popup-based dropdown select. Accepts a static options array or an async search function. Supports custom rendering via snippets for both the trigger display and dropdown items.
Import
import { Select } from '@atom-forge/ui';
import type { SelectOption, SelectOptionsSource } from '@atom-forge/ui';
Props
| Prop |
Type |
Default |
Description |
value |
string | number | undefined |
undefined |
Bindable selected value. |
options |
SelectOptionsSource |
[] |
Static array or async source object with search and get methods. |
placeholder |
string |
'Select...' |
Text shown when no value is selected. |
disabled |
boolean |
false |
Disables the trigger button. |
searchable |
boolean |
true |
Shows a search input inside the dropdown. |
button |
SelectButtonTrigger |
— |
Renders a Button component as the trigger instead of the select-field trigger. |
hideKeyboardHints |
boolean |
false |
Hides the keyboard-navigation hint bar at the bottom of the dropdown. |
clearable |
boolean |
false |
Shows an × button to clear the value. Selecting the same item again also clears it. |
compact |
boolean |
— |
Compact size (h-8). Mutually exclusive with small. |
small |
boolean |
— |
Small size (h-6). Mutually exclusive with compact. |
class |
string |
— |
Extra CSS classes on the trigger button. |
trigger |
Snippet<[SelectOption]> |
— |
Custom rendering of the selected value in the trigger button. |
option |
Snippet<[SelectOption, boolean]> |
— |
Custom rendering of each dropdown item. Receives (option, isHighlighted). |
Types
type SelectOption = { value: string | number; label: any };
type SelectOptionsSource =
| SelectOption[]
| {
search: (query: string) => Promise<SelectOption[]>;
get: (values: (string | number)[]) => Promise<SelectOption[]>;
};
type SelectButtonTrigger = {
label: string;
icon?: IconDefinition;
endIcon?: IconDefinition;
secondary?: boolean;
destructive?: boolean;
ghost?: boolean;
link?: boolean;
muted?: boolean;
accent?: boolean;
outline?: boolean;
pill?: boolean;
borderless?: boolean;
};
Usage
No option is highlighted when the dropdown opens. Press an arrow key to begin keyboard navigation.
Static options
<script lang="ts">
import { Select } from '@atom-forge/ui';
const options = [
{ value: 'apple', label: 'Apple' },
{ value: 'banana', label: 'Banana' },
{ value: 'cherry', label: 'Cherry' },
];
let value = $state<string | number | undefined>();
</script>
<Select {options} bind:value placeholder="Pick a fruit..."/>
Clearable
<Select {options} bind:value clearable/>
Hide keyboard hints
<Select {options} bind:value hideKeyboardHints/>
Use button when the trigger should be a Button rather than a select field. It accepts Button styling options; no chevron is shown unless you provide endIcon.
Variant flags remain optional booleans, including dynamic values and explicit false. The trigger forwards at most one active variant to Button: the first truthy flag in the order destructive → secondary → ghost → link → muted → accent. If none is truthy, it uses the primary style. Variant changes are reactive.
<Select
{options}
bind:value
button={{label: 'Filter', icon: Filter, secondary: true}}
/>
Async search
Pass an object with search and get methods as options.
search(query) is called on every query change.
get(values) resolves pre-selected values on load so the trigger can display the correct label.
<script lang="ts">
import { Select } from '@atom-forge/ui';
const source = {
search: async (q: string) => {
const res = await fetch(`/api/items?q=${encodeURIComponent(q)}`);
return res.json();
},
get: async (values: (string | number)[]) => {
const res = await fetch(`/api/items?ids=${values.join(',')}`);
return res.json();
},
};
let value = $state<string | number | undefined>();
</script>
<Select options={source} bind:value placeholder="Search..."/>
Custom rendering
Use a script-side caster for extra option fields; avoid assertions inside template expressions:
<script lang="ts">
import { Select, as } from '@atom-forge/ui';
import type { SelectOption } from '@atom-forge/ui';
const toColorOption = as<SelectOption & { dot: string }>;
const options = [
{ value: 'active', label: 'Active', dot: 'bg-accent' },
{ value: 'inactive', label: 'Inactive', dot: 'bg-muted' },
];
let value = $state<string | number | undefined>();
</script>
<Select {options} bind:value>
{#snippet trigger(opt)}
{@const colorOption = toColorOption(opt)}
<span class="flex items-center gap-2">
<span class="w-2 h-2 rounded-full {colorOption.dot}"></span>
{opt.label}
</span>
{/snippet}
{#snippet option(opt, isHighlighted)}
<div class={isHighlighted ? 'bg-accent text-accent-contrast' : 'hover:bg-secondary'}>
{opt.label}
</div>
{/snippet}
</Select>
Sizes
<Select {options} bind:value/>
<Select {options} bind:value compact/>
<Select {options} bind:value small/>
When to use
Use for one string or numeric ID from static options or a searchable async source, especially when custom option rendering is needed.
Alternatives
Use NativeSelect for a browser-native picker without search, Radio for visible single-choice options, or MultiSelect for several IDs.
Setup and behavior
Render below Root, which provides popup context and a PopupContainer. Bind value to $state<string | number | undefined>; undefined means no selection. Matching uses strict equality, so keep ID types consistent. Selection updates the binding and closes the popup; there is no public onchange prop. Static search compares String(label) case-insensitively. Async search(query) runs when the dropdown opens and on query changes, without built-in debounce; get([value]) resolves labels after mount and on value changes. Implement request throttling and error handling in the source. Label-resolution requests are not cancelled or guarded against stale responses. Button-trigger mode shows button.label, not the selected-label snippet or field clear button.
Select
A popup-based dropdown select. Accepts a static options array or an async search function. Supports custom rendering via snippets for both the trigger display and dropdown items.
Import
import { Select } from '@atom-forge/ui'; import type { SelectOption, SelectOptionsSource } from '@atom-forge/ui';Props
valuestring | number | undefinedundefinedoptionsSelectOptionsSource[]searchandgetmethods.placeholderstring'Select...'disabledbooleanfalsesearchablebooleantruebuttonSelectButtonTriggerhideKeyboardHintsbooleanfalseclearablebooleanfalsecompactbooleansmall.smallbooleancompact.classstringtriggerSnippet<[SelectOption]>optionSnippet<[SelectOption, boolean]>(option, isHighlighted).Types
type SelectOption = { value: string | number; label: any }; type SelectOptionsSource = | SelectOption[] | { search: (query: string) => Promise<SelectOption[]>; get: (values: (string | number)[]) => Promise<SelectOption[]>; }; type SelectButtonTrigger = { label: string; icon?: IconDefinition; endIcon?: IconDefinition; secondary?: boolean; destructive?: boolean; ghost?: boolean; link?: boolean; muted?: boolean; accent?: boolean; outline?: boolean; pill?: boolean; borderless?: boolean; };Usage
No option is highlighted when the dropdown opens. Press an arrow key to begin keyboard navigation.
Static options
<script lang="ts"> import { Select } from '@atom-forge/ui'; const options = [ { value: 'apple', label: 'Apple' }, { value: 'banana', label: 'Banana' }, { value: 'cherry', label: 'Cherry' }, ]; let value = $state<string | number | undefined>(); </script> <Select {options} bind:value placeholder="Pick a fruit..."/>Clearable
<Select {options} bind:value clearable/>Hide keyboard hints
<Select {options} bind:value hideKeyboardHints/>Button trigger
Use
buttonwhen the trigger should be a Button rather than a select field. It accepts Button styling options; no chevron is shown unless you provideendIcon.Variant flags remain optional booleans, including dynamic values and explicit
false. The trigger forwards at most one active variant to Button: the first truthy flag in the orderdestructive→secondary→ghost→link→muted→accent. If none is truthy, it uses the primary style. Variant changes are reactive.<Select {options} bind:value button={{label: 'Filter', icon: Filter, secondary: true}} />Async search
Pass an object with
searchandgetmethods asoptions.search(query)is called on every query change.get(values)resolves pre-selected values on load so the trigger can display the correct label.<script lang="ts"> import { Select } from '@atom-forge/ui'; const source = { search: async (q: string) => { const res = await fetch(`/api/items?q=${encodeURIComponent(q)}`); return res.json(); // SelectOption[] }, get: async (values: (string | number)[]) => { const res = await fetch(`/api/items?ids=${values.join(',')}`); return res.json(); // SelectOption[] }, }; let value = $state<string | number | undefined>(); </script> <Select options={source} bind:value placeholder="Search..."/>Custom rendering
Use a script-side caster for extra option fields; avoid assertions inside template expressions:
<script lang="ts"> import { Select, as } from '@atom-forge/ui'; import type { SelectOption } from '@atom-forge/ui'; const toColorOption = as<SelectOption & { dot: string }>; const options = [ { value: 'active', label: 'Active', dot: 'bg-accent' }, { value: 'inactive', label: 'Inactive', dot: 'bg-muted' }, ]; let value = $state<string | number | undefined>(); </script> <Select {options} bind:value> {#snippet trigger(opt)} {@const colorOption = toColorOption(opt)} <span class="flex items-center gap-2"> <span class="w-2 h-2 rounded-full {colorOption.dot}"></span> {opt.label} </span> {/snippet} {#snippet option(opt, isHighlighted)} <div class={isHighlighted ? 'bg-accent text-accent-contrast' : 'hover:bg-secondary'}> {opt.label} </div> {/snippet} </Select>Sizes
<Select {options} bind:value/> <!-- normal (h-10) --> <Select {options} bind:value compact/> <!-- h-8 --> <Select {options} bind:value small/> <!-- h-6 -->When to use
Use for one string or numeric ID from static options or a searchable async source, especially when custom option rendering is needed.
Alternatives
Use NativeSelect for a browser-native picker without search, Radio for visible single-choice options, or MultiSelect for several IDs.
Setup and behavior
Render below
Root, which provides popup context and a PopupContainer. Bindvalueto$state<string | number | undefined>;undefinedmeans no selection. Matching uses strict equality, so keep ID types consistent. Selection updates the binding and closes the popup; there is no publiconchangeprop. Static search comparesString(label)case-insensitively. Asyncsearch(query)runs when the dropdown opens and on query changes, without built-in debounce;get([value])resolves labels after mount and on value changes. Implement request throttling and error handling in the source. Label-resolution requests are not cancelled or guarded against stale responses. Button-trigger mode showsbutton.label, not the selected-label snippet or field clear button.