Native Select#

A real <select> wrapped in zero anatomy. The platform owns the popup, the keyboard and the accessibility tree — mobile wheels, the OS-native picker, form restoration — and zero owns the styleable wrapper: a carrier span, the <select> itself, and a replacement chevron for recipes that set appearance: none.

Import#

TSX
import { NativeSelect } from '@sigx/zero/native-select';

NativeSelect is a compound with a single member, NativeSelect.Root; <NativeSelect> is also callable directly. It is also re-exported from the @sigx/zero root, together with nativeSelectAnatomy.

Usage#

TSX
import { component } from 'sigx';
import { NativeSelect } from '@sigx/zero/native-select';

const PetPicker = component(({ signal }) => {
    const state = signal({ pet: '' });

    return () => (
        <NativeSelect.Root
            model={() => state.pet}
            placeholder="Pick a pet…"
            name="pet"
            options={[
                { value: 'cat', label: 'Cat' },
                { value: 'dog', label: 'Dog' },
                { value: 'parrot', label: 'Parrot', group: 'Birds' },
            ]}
        />
    );
});

model={() => state.pet} binds the selected value both ways — a change in the picker writes state.pet, and writing state.pet selects the option. Leave the model off and pass defaultValue to keep the state inside the component; valueChange fires either way. See Models.

options renders real <option> elements, and a real <optgroup> per distinct group in first-appearance order, with label defaulting to value — the same array shape and grouping walk as Select's sugar.

The placeholder#

placeholder renders as the conventional disabled empty first option, and drives the data-placeholder flag while the value is empty — what lets a recipe grey the resting text without inventing a state for it.

Without a placeholder, "nothing chosen" is not representable: a <select> with no empty option always has a value (the platform rests on, and posts, the first option). An empty model is therefore coerced to the control's actual value on mount, so the model and the posted value stay one truth. Represent "nothing chosen yet" with placeholder.

Hand-written options#

TSX
<NativeSelect.Root model={() => state.pet} placeholder="Pick a pet…">
    <option value="cat">Cat</option>
    <optgroup label="Birds">
        <option value="parrot">Parrot</option>
    </optgroup>
</NativeSelect.Root>

Slot children win entirely over options when both are given — never merged.

Inside a Field#

TSX
<Field.Root>
    <Field.Label>Pet</Field.Label>
    <NativeSelect.Root model={() => state.pet} placeholder="Pick a pet…" options={pets} />
    <Field.Description>Used on your profile.</Field.Description>
</Field.Root>

A <select> is labelable, so Field.Label names it through for: the control adopts the field's control id, its disabled / invalid / required flags and aria-describedby. See Field.

Anatomy#

PartElementStatesFlagsNotes
rootspandisabled, invalid, required, placeholder, focus-visibleCarries the variant axes; the chevron overlays it.
controlselectdisabled, invalid, required, placeholder, focus-visibleThe form control: carries name, disabled, required, aria-invalid, aria-describedby. Inside root.
indicatorspanaria-hidden; the replacement chevron, defaulting to a chevron glyph. Inside root.

Every part carries data-scope="native-select" and data-part="<part>". There is no data-state anywhere on this scope: the platform renders the popup, so open / closed never exists in zero's DOM — everything is a flag, and the same five flags are stamped on both root and control. There is no hidden input either: the visible <select> is the form control and carries name. Recipes set appearance: none on control, draw the well (border, padding, focus ring) there, and paint the indicator in the place of the platform arrow. See The anatomy contract.

Props#

NativeSelect.Root#

PropTypeDefaultDescription
modelstringTwo-way binding of the selected option's value.
defaultValuestring''Initial value when uncontrolled.
valueChangeevent (value: string)Fires whenever the selection changes.
optionsReadonlyArray<OptionInput>{ value, label?, disabled?, group? }[]; rendered as real <option> / <optgroup> elements when there are no slot children.
placeholderstringRendered as the disabled empty first option; drives data-placeholder while the value is empty.
namestringForm field name, rendered on the <select>.
requiredbooleanfalseRenders required and data-required.
invalidbooleanfalseRenders aria-invalid and data-invalid.
disabledbooleanfalseRenders disabled and data-disabled.
color / size / variant / axes / modsdesign-system vocabularyThe variant axes, rendered as data-* on root.
classstringExtra classes on the root element.

Field context (disabled, invalid, required, aria-describedby, the control id) is merged with the props.

Keyboard#

The platform's own: the <select> opens and navigates natively, and nothing is added or intercepted. Focus rings draw off data-focus-visible, mirrored onto the root so a recipe can ring the wrapper.

In the shipped design systems#

Both @sigx/zero-basic and @sigx/zero-daisyui wire color (all eight recommended roles) and size (xsxl) on native-select, so <NativeSelect.Root color="primary" size="sm"> is styled in both. Neither wires a variant on the scope; under a design system's /register import the prop is therefore absent. Both set appearance: none on the control and paint the indicator as the chevron. See Typed vocabulary.

Select for a styleable listbox, Field for labelling.