Skip to main content

Select

Info:Form html svelte astro Success: coverage 18/18

A styled native dropdown: `.xoji-control` chrome, a custom chevron, and a label, with valid and invalid states across three sizes.

Live demo

live · @xoji/astro

Select

Labelled selects

Sizes

States

Pick a region to continue.

Select is a thin, accessible skin over the native <select>. It inherits the shared .xoji-control chrome (the same fill, border, radius, and focus ring as Field) so it sits flush beside other form controls, then hides the platform arrow and paints its own chevron, colored by focus, invalid, and disabled state.

Options are plain <option> / <optgroup> children passed straight through to the native element, so keyboard navigation, type-ahead, and the OS picker all come for free. A label wires an accessible name, invalid plus error surface validation, and sm / md / lg tune the density.

When to use

How this component composes with the rest of the set.

Inherits .xoji-control chrome from the base layer, so it lines up pixel-for-pixel with Field and Textarea.
Pair with Field for text inputs and Button for the submit action to build a complete form row.
Options are native <option> / <optgroup> elements. No custom item component required.

Props

8 props, straight from the manifest.

PropTypeDefaultBindingsDescription
label string
html svelte astro
Visible label and accessible name. When empty, falls back to `aria-label`.
value string
html svelte astro
The selected option's value. Two-way bindable in Svelte.
size Size
sm md lg
md
html svelte astro
Control density.
name string
html svelte astro
Form field name, forwarded to the native select for submission.
invalid boolean false
html svelte astro
Marks the field invalid: danger border, danger chevron, and `aria-invalid`.
error string
html svelte astro
Validation message shown beneath the control; wired via `aria-describedby` when invalid.
disabled boolean false
html svelte astro
Disables the native select and mutes the chevron.
required boolean false
html svelte astro
Marks the field required, reflecting `required` and `aria-required` onto the native select.

Appearance

Variants

default

.xoji-select

The standard control: .xoji-control chrome with the accent focus ring.

invalid

.xoji-select--invalid

Validation-failed treatment: danger border and chevron, danger-tinted focus ring.

Sizes

sm

.xoji-select--sm

Compact.

md

default
.xoji-select

Default.

lg

.xoji-select--lg

Large.

States

focus-visible

.xoji-select__field:focus-visible + .xoji-select__chevron

Keyboard focus: the .xoji-control accent ring, with the chevron tinted to match.

invalid-focus

.xoji-select--invalid .xoji-select__field:focus-visible

Focus while invalid: the danger border holds and the ring shifts to the danger tint.

disabled

.xoji-select__field:disabled + .xoji-select__chevron

Non-interactive: the native control mutes and the chevron drops to the disabled ink.

Anatomy

The named parts that make up the component, with their selectors.

root

.xoji-select

The vertical stack holding the label, control, and error message.

--space-1 --font-sans

label

.xoji-select__label

The control's visible label, hidden when no label is set.

--text-sm --weight-medium --fg-1 --leading-normal

control

.xoji-select__control

The positioning wrapper that overlays the chevron on the native select.

select

.xoji-select__field

The native <select> carrying the shared .xoji-control chrome with the platform arrow suppressed.

--space-7

chevron

.xoji-select__chevron

The custom dropdown indicator, decorative and pointer-transparent, recolored by state.

--space-3 --fg-2 --accent --duration-fast --ease-standard

error

.xoji-select__error

The validation message shown when invalid and an error string are set.

--text-sm --leading-normal --danger

Tokens & coverage

What the component consumes, checked live against what the algorithm produces.

Success:fully covered 18/18 consumed tokens produced default register: 276 tokens

Live coverage check against the xoji-default register (derive(xojiDefault, { anchors })coverComponent(manifest, register)). Every token this component consumes must be a key the algorithm produces.

--accent --border-normal --danger --danger-bg --danger-text --duration-fast --ease-standard --fg-1 --fg-2 --fg-disabled --font-sans --leading-normal --space-1 --space-3 --space-7 --text-lg --text-sm --weight-medium

Slots

default
html svelte astro

The <option> and <optgroup> elements, passed straight through to the native select.

Accessibility

Renders a native <select>, so keyboard navigation, type-ahead, and the OS picker work without any custom scripting.
label provides the accessible name via a for-linked <label>; when absent, an aria-label is required and the html binding warns at runtime when neither is present.
invalid sets aria-invalid="true"; when an error string is present it is linked with aria-describedby.
required reflects both the native required attribute and aria-required for assistive tech.
The chevron is decorative (aria-hidden); selection state lives in the native control, not the visual.
Focus is shown with the shared .xoji-control ring plus a transparent outline that the forced-colors base rule promotes to a real system outline.

Code

Labels, groups, and validation

A plain labelled select, a grouped large select, and an invalid required select with an error message.

<xoji-select label="Theme" name="theme" value="auto">
	<option value="auto">Match system</option>
	<option value="light">Light</option>
	<option value="dark">Dark</option>
</xoji-select>

<xoji-select label="Plan" size="lg">
	<optgroup label="Personal">
		<option value="free">Free</option>
		<option value="pro">Pro</option>
	</optgroup>
	<optgroup label="Teams">
		<option value="team">Team</option>
		<option value="org">Organization</option>
	</optgroup>
</xoji-select>

<xoji-select label="Country" invalid error="Select a country to continue." required>
	<option value="" disabled selected>Choose…</option>
	<option value="us">United States</option>
	<option value="ca">Canada</option>
</xoji-select>
<script lang="ts">
	import { Select } from "@xoji/svelte";
	let theme = $state("auto");
</script>

<Select label="Theme" name="theme" bind:value={theme}>
	<option value="auto">Match system</option>
	<option value="light">Light</option>
	<option value="dark">Dark</option>
</Select>

<Select label="Plan" size="lg">
	<optgroup label="Personal">
		<option value="free">Free</option>
		<option value="pro">Pro</option>
	</optgroup>
	<optgroup label="Teams">
		<option value="team">Team</option>
		<option value="org">Organization</option>
	</optgroup>
</Select>

<Select label="Country" invalid error="Select a country to continue." required>
	<option value="" disabled selected>Choose…</option>
	<option value="us">United States</option>
	<option value="ca">Canada</option>
</Select>
---
import { Select } from "@xoji/astro";
---

<Select label="Theme" name="theme" value="auto">
	<option value="auto">Match system</option>
	<option value="light">Light</option>
	<option value="dark">Dark</option>
</Select>

<Select label="Plan" size="lg">
	<optgroup label="Personal">
		<option value="free">Free</option>
		<option value="pro">Pro</option>
	</optgroup>
	<optgroup label="Teams">
		<option value="team">Team</option>
		<option value="org">Organization</option>
	</optgroup>
</Select>

<Select label="Country" invalid error="Select a country to continue." required>
	<option value="" disabled selected>Choose…</option>
	<option value="us">United States</option>
	<option value="ca">Canada</option>
</Select>