registry:ui · Data Entry
native-select — the platform popup, kept
The original native <select> with appearance-none and self-drawn chevron — recommended for simple forms, mobile native overlay picker, and FormData submission. Multiple mode with listbox geometry.
native-select
the simple-scenario select: a real <select> under the jx-pure face — the platform's overlay picker on touch, a real name/value pair in FormData.
<NativeSelect
label="plan"
name="plan"
bind:value
>
<option value="free">free</option>
<option value="pro">pro</option>
<option value="team">team</option>
</NativeSelect>native-select
Native first — the default you should ship
A real <select> whose popup list, keyboard, and type-ahead belong to the platform. The repaint touches only the closed control: appearance-none plus an inline SVG chevron, absolutely positioned and pointer-events: none. It rides into FormData with a name/value pair and gets the OS overlay picker on mobile.
native-select · native first
node<script lang="ts">
import NativeSelect from '@ui/native-select.svelte';
import CardGrid from '$lib/ui/card-grid/card-grid.svelte';
let runtimeNative = $state('node');
</script>
<CardGrid min="230px">
<div class="demo-cell flex flex-col gap-3" data-no-subgrid>
<NativeSelect
label="runtime — native popup"
name="cmp_runtime"
value={runtimeNative}
onchange={(event) => (runtimeNative = event.currentTarget.value)}
>
<option value="node">node</option>
<option value="bun">bun</option>
<option value="deno">deno</option>
</NativeSelect>
<span class="text-muted-foreground text-[12.5px]">
platform popup · FormData-ready · bound value:
<code class="text-accent">{runtimeNative}</code>
</span>
</div>
<div class="demo-cell flex flex-col gap-3" data-no-subgrid>
<NativeSelect label="disabled" name="demo_select_disabled" disabled>
<option>frozen</option>
</NativeSelect>
<span class="text-muted-foreground text-[12.5px]">
the whole control freezes — options and all
</span>
</div>
<div class="demo-cell flex flex-col gap-3" data-no-subgrid>
<NativeSelect label="multiple (list box)" name="demo_targets" multiple>
<option value="linux">linux</option>
<option value="macos">macos</option>
<option value="windows">windows</option>
</NativeSelect>
<span class="text-muted-foreground text-[12.5px]">
native list-box geometry · ctrl/cmd multi-selects
</span>
</div>
</CardGrid>When the native popup can't say what you need — per-option descriptions, a painted
panel, roving highlight — reach for the rich sibling: select builds the same trigger paint on a popover listbox. The split changes nothing
semantically: both carry label[for] and the family
error law.
<!-- the simple-scenario select: a real name/value pair in FormData,
the platform's overlay picker on touch -->
<NativeSelect label="plan" name="plan">
<option value="free">free</option>
<option value="pro">pro</option>
</NativeSelect>
<!-- multiple renders the native list-box geometry -->
<NativeSelect label="targets" name="targets" multiple>
<option value="linux">linux</option>
<option value="macos">macos</option>
</NativeSelect>law
label + error wiring
The error prop is pure semantics: it sets aria-invalid='true', wires aria-describedby to the “! message” line, and dashes the shell border — a monochrome invalid signal, because the one-hue law has no error red.
plan is required
<NativeSelect label="plan" error="plan is required">
<option value="">— choose —</option>
<option value="free">free</option>
</NativeSelect>
<!-- error wiring, the family law:
· aria-invalid="true" on the control
· aria-describedby → the "! message" line
· border-style: dashed on the shell (monochrome invalid signal —
the one-hue law has no error red) -->composition
In a submitted form
NativeSelect is the select in a submitted form — its name/value pair is the point. Uncontrolled field, native constraint validation (the required bubble belongs to the platform), FormData read once at submit.
native-select · in a form
sign up — the FormData payload prints here<script lang="ts">
import NativeSelect from '@ui/native-select.svelte';
import PressButton from '@ui/press-button.svelte';
import TerminalCard from '$lib/ui/terminal-card/terminal-card.svelte';
let result = $state<{ outputs: string[] } | null>(null);
function onSubmit(event: SubmitEvent) {
event.preventDefault();
const data = new FormData(event.currentTarget as HTMLFormElement);
const outputs: string[] = [];
for (const [key, value] of data) {
if (typeof value === 'string' && value !== '') outputs.push(`${key}: ${value}`);
}
outputs.push('form submitted ✓');
result = { outputs };
}
</script>
<div class="grid gap-6 min-[900px]:grid-cols-[minmax(0,1fr)_minmax(0,0.9fr)]">
<form class="flex flex-col gap-4" aria-label="plan" onsubmit={onSubmit}>
<NativeSelect label="plan" name="plan" required>
<option value="">— choose a plan —</option>
<option value="free">free — community</option>
<option value="pro">pro — dedicated backend</option>
</NativeSelect>
<div class="flex flex-wrap items-center gap-3 pt-1">
<PressButton type="submit" variant="fill">sign up</PressButton>
<span class="text-muted-foreground text-[12.5px]">
required fields use native validation — try submitting empty
</span>
</div>
</form>
<div>
{#if result}
{#key result}
<TerminalCard
barTitle="form — zsh"
command="form.submit"
outputs={result.outputs}
/>
{/key}
{:else}
<div class="border-border bg-muted/40 text-muted-foreground flex h-full min-h-40 flex-col items-center justify-center gap-2 border p-6 text-center text-[13px]">
<span class="font-nav text-primary text-[11px] uppercase tracking-[0.24em]">awaiting submit</span>
<span>choose a plan and press <code class="text-accent">sign up</code> — the FormData payload prints here</span>
</div>
{/if}
</div>
</div>types
NativeSelect variants
The single popup select, the multiple list-box posture, the error state, and the disabled field.
native-select · variants
plan is required
<script lang="ts">
import NativeSelect from '@ui/native-select.svelte';
</script>
<div class="grid gap-4 sm:grid-cols-2">
<div class="border border-border p-4">
<NativeSelect label="single" name="types-single">
<option value="node">node</option>
<option value="bun">bun</option>
<option value="deno">deno</option>
</NativeSelect>
</div>
<div class="border border-border p-4">
<NativeSelect label="multiple (list box)" name="types-multiple" multiple>
<option value="linux">linux</option>
<option value="macos">macos</option>
<option value="windows">windows</option>
</NativeSelect>
</div>
<div class="border border-border p-4">
<NativeSelect label="error" name="types-error" error="plan is required">
<option value="">— choose —</option>
<option value="free">free</option>
</NativeSelect>
</div>
<div class="border border-border p-4">
<NativeSelect label="disabled" name="types-disabled" disabled>
<option>frozen</option>
</NativeSelect>
</div>
</div>usage
Usage
Options arrive as <option>/<optgroup> children; the popup, keyboard navigation, and type-ahead stay the platform's.
<!-- the simple-scenario select: a real name/value pair in FormData,
the platform's overlay picker on touch -->
<NativeSelect label="plan" name="plan">
<option value="free">free</option>
<option value="pro">pro</option>
</NativeSelect>
<!-- multiple renders the native list-box geometry -->
<NativeSelect label="targets" name="targets" multiple>
<option value="linux">linux</option>
<option value="macos">macos</option>
</NativeSelect>a11y
Accessibility
The native select keeps every platform behavior; the component wires the label and the validation message to the control.
Keyboard
| Key | Action |
|---|---|
| Tab | Moves focus to the select |
| ↑ / ↓ | Steps through options in the platform popup |
| type-ahead | Jumps to the option starting with the typed text |
| Ctrl / Cmd | Multi-selects rows in the multiple list-box mode |
ARIA
| Attribute | Value | Description |
|---|---|---|
| aria-invalid | 'true' | Set on the native select when the error prop is provided |
| aria-describedby | {id}-error | Points at the "! message" validation line |
theming
Density and tokens
The closed-control shell, chevron, and label rhythm are density-scope tokens; resize the scope and the whole field follows (the popup stays the platform's).
| Token | Default | Source |
|---|---|---|
| --jx-hit | 28 / 32 / 40 / 48px | density |
| --jx-text | 11 / 12 / 13 / 15px | density |
| --jx-icon | 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 — named steps · auto (inherit; stamps nothing) · exact numbers · query(). The family CONSUMES size and color (the native element never receives them, §1); native-select keeps the native size attribute as the rows-count passthrough instead of the axis.
native-select · universal props
<!-- the universal axes EXCEPT size: the native 'size' attribute stays
the platform's rows-count passthrough (multiple-rows listbox mode)
— the family documents the collision instead of consuming it -->
<NativeSelect label="density small" density="small" radius="medium">
<option>japan</option>
</NativeSelect>
<NativeSelect label="listbox" multiple size={3}>
<option>japan</option>
</NativeSelect>api
API
Props extend the native HTML select attributes; the entries below are the component-owned additions. Everything else (name, disabled, required, multiple, size…) rides through restProps.
Properties
| Property | Type | Default | Description |
|---|---|---|---|
| children * | Snippet | — | The <option> / <optgroup> list, authored by the caller. |
| label | string | — | Field label rendered as label[for] above the control. |
| error | string | — | Error text: sets aria-invalid, wires aria-describedby, dashes the shell. |
value bind | string | string[] | — | Bindable; bound ⇒ controlled two-way, absent ⇒ uncontrolled native select. |
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) |