jixoai-ui
the terminal design language
loading the style sheet…
Skip to content
docs · print medium: screen

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.

real FormData pair mobile overlay picker appearance-none repaint multiple list-box mode zero deps · Svelte 5 runes

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.

native-select-usage.svelte svelte
<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

platform popup · FormData-ready · bound value: node
the whole control freezes — options and all
native list-box geometry · ctrl/cmd multi-selects
native-select-native-first-demo.svelte svelte
<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.

NativeSelect usage
<!-- 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

error
<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

required fields use native validation — try submitting empty
awaiting submit choose a plan and press sign up — the FormData payload prints here
native-select-in-a-form-demo.svelte svelte
<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

native-select-types-demo.svelte svelte
<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.

NativeSelect usage
<!-- 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

KeyAction
TabMoves focus to the select
↑ / ↓Steps through options in the platform popup
type-aheadJumps to the option starting with the typed text
Ctrl / CmdMulti-selects rows in the multiple list-box mode

ARIA

AttributeValueDescription
aria-invalid'true'Set on the native select when the error prop is provided
aria-describedby{id}-errorPoints 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).

xs
sm
default
lg
TokenDefaultSource
--jx-hit28 / 32 / 40 / 48pxdensity
--jx-text11 / 12 / 13 / 15pxdensity
--jx-icon16 / 18 / 20 / 24pxdensity
--jx-inset8 / 8 / 12 / 16pxdensity
--jx-gap8 / 8 / 12 / 16pxdensity

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

native-select-universal.svelte svelte
<!-- 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

PropertyTypeDefaultDescription
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 bindstring | string[]—Bindable; bound ⇒ controlled two-way, absent ⇒ uncontrolled native select.

Universal props

PropertyTypeDefaultDescription
size'small' | 'medium' | 'large' | 'auto' | number'auto' · ambient scopethe base scale — root font-size; parts size in em
shape'round' | 'scoop' | 'bevel' | 'notch' | 'square' | 'squircle' | 'auto''auto' · ambient scopecorner geometry (CSS corner-shape; §14 degrade table)
radius'small' | 'medium' | 'large' | 'auto' | number'auto' · ambient scopecorner size; auto = the concentric broadcast (§3)
density'small' | 'medium' | 'large' | 'auto' | number'auto' · ambient scopespacing/leading scale over the kernel channels (§4)
color'primary' | 'secondary' | 'error' | 'warn' | 'success' | 'info' | 'auto' | number | string'auto' · ambient scopethe hue axis of the fixed oklch primary system (§5)
theme'light' | 'dark' | 'system' | 'auto''auto' · ambient scopelight/dark profile; system = the JS-mutable global (§6)
elevation'level-1' | 'level0' | 'level1' | 'level2' | 'level3' | 'level4' | 'level5' | 'auto' | number'auto' · ambient scopeofficial M3 levels over the surface ladder (§7)
motion'reduced' | 'subtle' | 'normal' | 'expressive' | 'auto' | number'auto' · ambient scopeintensity across the motion kernels (§8)
jixoai-ui ui.jixoai.com the jixoai design language

Theme

Brand hue

0°

Auto cycle · 24h