registry:ui · Data Entry
select — the popover listbox
Custom select built on the native Popover API + CSS Anchor Positioning: popover='auto' light-dismiss panel, listbox role with roving aria-activedescendant (↑↓/Home/End/Enter), terminal bezel styling with primary left-border selected state, RTL-aware. name= submits the committed VALUE into FormData through the faceless form-field bridge (ElementInternals); for the platform's own overlay picker use native-select instead. Floating-surface law: ::after shadow layer (semi-transparent black/white) + @starting-style entry; variant='solid' | 'acrylic' | 'auto' (acrylic falls back to solid under prefers-reduced-transparency; the terminal bezel keeps its identity through the jx-surface fill props).
$npx jixoai-ui add select
https://ui.jixoai.com/r/select.json
usage
Usage
Bind the committed value; rows with descriptions and disabled entries are plain data on the options array.
const runtimeOptions: SelectOption[] = [
{ value: 'node', label: 'node', description: 'node-pty backend' },
{ value: 'bun', label: 'bun', description: 'Bun.Terminal, 1.3.13+' },
{ value: 'deno', label: 'deno', description: '@sigma/pty-ffi over FFI' },
{ value: 'wasi', label: 'wasi (not yet)', disabled: true },
];
<Select
label="runtime"
placeholder="pick a runtime…"
bind:value={runtime}
options={runtimeOptions}
/>select
The popover-listbox select: per-option descriptions, ↑/↓/Home/End/Enter roving highlight with focus restitution — for when the native popup can't say what you need.
- node node-pty backend — ConPTY on windows, forkpty elsewhere
- bun Bun.Terminal — linux/macos since 1.3.13, windows 1.3.14
- deno @sigma/pty-ffi — FFI over rust portable-pty
- wasi — coming soon reserved route, not implemented yet
<Select
label="runtime"
placeholder="pick a runtime…"
options={runtimeOptions}
bind:value
/>select × 2
One family, two selects — native first
NativeSelect is the default you should ship: a real <select> (options as <option> children) whose popup list, keyboard, and type-ahead belong to the platform — it rides into FormData with a name/value pair and gets the OS overlay picker on mobile. Select is the same trigger paint on a <button> opening a popover listbox: per-option descriptions, a painted terminal-bezel panel, roving ↑/↓/Enter highlight with focus restitution — reach for it only when the native popup can't say what you need.
node- node node-pty backend — ConPTY on windows, forkpty elsewhere
- bun Bun.Terminal — linux/macos since 1.3.13, windows 1.3.14
- deno @sigma/pty-ffi — FFI over rust portable-pty
- wasi — coming soon reserved route, not implemented yet
nodeOpen the right one: the panel is popover="auto" wired
with popovertarget, so light dismiss, Escape, and
top-layer rendering are the browser's; the JS only drives role="listbox" / role="option" / aria-activedescendant, the ↑/↓/Home/End/Enter
highlight, and focus restitution to the trigger on every close path. The selected row
reads --terminal-hover fill with a 2px --primary edge on border-inline-start — under dir="rtl" the edge flips sides by itself.
label + error wiring, both selects
The split changes nothing semantically: label[for] binds to the control (the <button> trigger in Select's case), and the error prop wires aria-invalid + aria-describedby + the dashed shell — the same
monochrome invalid signal as the rest of the family.
plan is required
- — choose —
- free community tier
- pro dedicated backend
plan is required
const runtimeOptions: SelectOption[] = [
{ value: 'node', label: 'node', description: 'node-pty backend' },
{ value: 'bun', label: 'bun', description: 'Bun.Terminal, 1.3.13+' },
{ value: 'deno', label: 'deno', description: '@sigma/pty-ffi over FFI' },
{ value: 'wasi', label: 'wasi (not yet)', disabled: true },
];
<Select
label="runtime"
placeholder="pick a runtime…"
bind:value={runtime}
options={runtimeOptions}
/>geometry
RTL — geometry from logical properties
Nothing in the component branches on direction: the chevron sits in the flex flow and the selected-row edge is border-inline-start. The writing mode does the rest.
select · rtl
- node node-pty backend — ConPTY on windows, forkpty elsewhere
- bun Bun.Terminal — linux/macos since 1.3.13, windows 1.3.14
- deno @sigma/pty-ffi — FFI over rust portable-pty
<script lang="ts">
import Select from '@ui/select.svelte';
</script>
<div dir="rtl" class={cx(rt.flex, rt.col, rt.gap16, rt.frame, rt.p16)}>
<Select
label="runtime (rtl)"
bind:value={runtimeRtl}
options={runtimeOptions.slice(0, 3)}
placeholder="pick…"
/>
<span class={cx(rt.inkMuted, rt.text12)}>
dir="rtl" on the wrapper — trigger chevron and panel edge line flipped without a
physical property in sight
</span>
</div>The chevron sits in the flex flow, the selected-row edge is border-inline-start, and the panel anchors with CSS
Anchor Positioning whose offsets are logical too. The writing mode does the rest.
types
Select variants
The popover listbox with plain rows, rows carrying descriptions, a disabled row, and the error state.
select · variants
- node node-pty backend — ConPTY on windows, forkpty elsewhere
- bun Bun.Terminal — linux/macos since 1.3.13, windows 1.3.14
- deno @sigma/pty-ffi — FFI over rust portable-pty
- wasi — coming soon reserved route, not implemented yet
- — choose —
- free
- pro
plan is required
<script lang="ts">
import Select from '@ui/select.svelte';
</script>
<div class="grid gap-4 sm:grid-cols-2">
<div class="border border-border p-4">
<Select
label="descriptions"
options={runtimeOptions}
placeholder="open for second lines…"
/>
</div>
<div class="border border-border p-4">
<Select
label="error"
options={[
{ value: '', label: '— choose —' },
{ value: 'free', label: 'free' },
{ value: 'pro', label: 'pro' },
]}
error="plan is required"
/>
</div>
</div>a11y
Accessibility
A real listbox contract on top of the native popover: roving highlight via aria-activedescendant, focus restitution to the trigger on every close path.
Keyboard
| Key | Action |
|---|---|
| Tab | Moves focus to the trigger button |
| ↑ / ↓ (closed) | Opens the panel, like the native select |
| ↑ / ↓ (open) | Moves the highlight, skipping disabled rows; no wrap |
| Home / End | Jumps to the first / last enabled row |
| Enter / Space | Chooses the highlighted row and closes the panel |
| Esc / outside click | Light dismiss from popover="auto"; focus returns to the trigger |
ARIA
| Attribute | Value | Description |
|---|---|---|
| aria-haspopup | "listbox" | On the trigger button, with aria-expanded synced live |
| aria-activedescendant | {id}-opt-{index} | Roving highlight ID on the focusable list |
| aria-selected | 'true' / 'false' | On each role="option" row |
| aria-invalid | 'true' | On the trigger when the error prop is provided |
| aria-describedby | {id}-error | Points at the "! message" validation line |
theming
Density and tokens
Trigger and panel rows share the density-scope rhythm; resize the scope and the trigger, rows, and label stack resize together.
- node node-pty backend — ConPTY on windows, forkpty elsewhere
- bun Bun.Terminal — linux/macos since 1.3.13, windows 1.3.14
- deno @sigma/pty-ffi — FFI over rust portable-pty
- wasi — coming soon reserved route, not implemented yet
- node node-pty backend — ConPTY on windows, forkpty elsewhere
- bun Bun.Terminal — linux/macos since 1.3.13, windows 1.3.14
- deno @sigma/pty-ffi — FFI over rust portable-pty
- wasi — coming soon reserved route, not implemented yet
- node node-pty backend — ConPTY on windows, forkpty elsewhere
- bun Bun.Terminal — linux/macos since 1.3.13, windows 1.3.14
- deno @sigma/pty-ffi — FFI over rust portable-pty
- wasi — coming soon reserved route, not implemented yet
- node node-pty backend — ConPTY on windows, forkpty elsewhere
- bun Bun.Terminal — linux/macos since 1.3.13, windows 1.3.14
- deno @sigma/pty-ffi — FFI over rust portable-pty
- wasi — coming soon reserved route, not implemented yet
| Token | Default | Source |
|---|---|---|
| --jx-hit | 28 / 32 / 40 / 48px | density |
| --jx-text | 11 / 12 / 13 / 15px | density |
| --jx-line | 16 / 18 / 20 / 24px | density |
| --jx-inset | 8 / 8 / 12 / 16px | density |
| --jx-gap | 8 / 8 / 12 / 16px | density |
axes
Universal props
The eight-axis surface (explicit-props): size · shape · radius · density · color · theme · elevation · motion — each axis takes named steps, auto (inherit the ambient context; stamps nothing), an exact number (px · coefficient · dp · hue per axis), or query() for responsive/container-conditional values. The TRIGGER surface (.jx-field) and the PORTALED listbox both carry the resolved lanes (the batch C portal law: the panel stamps its own carriers, self-carried across the top-layer promotion); elevation carries the family own level2 — the anchored panel's menu rung. Open a demo listbox to see the panel's carriers live.
Select · universal props
- node node-pty backend — ConPTY on windows, forkpty elsewhere
- bun Bun.Terminal — linux/macos since 1.3.13, windows 1.3.14
- deno @sigma/pty-ffi — FFI over rust portable-pty
- wasi — coming soon reserved route, not implemented yet
- node node-pty backend — ConPTY on windows, forkpty elsewhere
- bun Bun.Terminal — linux/macos since 1.3.13, windows 1.3.14
- deno @sigma/pty-ffi — FFI over rust portable-pty
- wasi — coming soon reserved route, not implemented yet
<Select label="runtime" options={runtimeOptions} size={18} density="small" />api
API
Props extend native button attributes on the trigger; form submission rides the faceless jx-form-field bridge (the committed value, never the label, reaches FormData).
Properties
| Property | Type | Default | Description |
|---|---|---|---|
| options * | SelectOption[] | — | The full option list; order = panel order. |
value bind | string | — | Committed value; undefined shows the placeholder. |
| placeholder | string | 'Select...' | Trigger text when nothing is selected. |
| label | string | — | Field label rendered as label[for] above the trigger. |
| name | string | — | Form field name — the bridge submits the committed value under it. |
| error | string | — | Error text: sets aria-invalid, wires aria-describedby, dashes the shell. |
| multiple | boolean | false | Reserved extension direction — not implemented in v1 (warns). |
| variant | 'solid' | 'acrylic' | 'auto' | 'auto' · Own default, not ambient | Floating-surface fill of the panel. |
| onchange | (value: string) => void | — | Commit hook (issue #6): fires with the newly committed value on every commit route (click, Enter, Space), alongside the bind:value write — sugar over bind:value. The native change event never fires on the trigger button, so this prop owns the channel outright. |
| disabled | boolean | false | Disables the trigger and the form-bridge field. |
Universal props
| Property | Type | Default | Description |
|---|---|---|---|
| size | 'small' | 'medium' | 'large' | 'auto' | number | 'auto' · ambient scope | the base scale — root font-size; parts size in em |
| shape | 'round' | 'scoop' | 'bevel' | 'notch' | 'square' | 'squircle' | 'auto' | 'auto' · ambient scope | corner geometry (CSS corner-shape; §14 degrade table) |
| radius | 'small' | 'medium' | 'large' | 'auto' | number | 'auto' · ambient scope | corner size; auto = the concentric broadcast (§3) |
| density | 'small' | 'medium' | 'large' | 'auto' | number | 'auto' · ambient scope | spacing/leading scale over the kernel channels (§4) |
| color | 'primary' | 'secondary' | 'error' | 'warn' | 'success' | 'info' | 'auto' | number | string | 'auto' · ambient scope | the hue axis of the fixed oklch primary system (§5) |
| theme | 'light' | 'dark' | 'system' | 'auto' | 'auto' · ambient scope | light/dark profile; system = the JS-mutable global (§6) |
| elevation | 'level-1' | 'level0' | 'level1' | 'level2' | 'level3' | 'level4' | 'level5' | 'auto' | number | 'auto' · ambient scope | official M3 levels over the surface ladder (§7) |
| motion | 'reduced' | 'subtle' | 'normal' | 'expressive' | 'auto' | number | 'auto' · ambient scope | intensity across the motion kernels (§8) |