Native Select
The platform selection control, dressed lightly while retaining native behavior.
- Family
- Choice
- Available import
@/components/ui/native-select- Status
- Thorbis wrapper and live specimen ship here.
native-select- Use
- Use first for a short stable option list, especially on mobile.
- Avoid
- Do not replace it with a custom popup unless search or rich options are truly needed.
Use this component
GitHub source links require access to the private Thorbis design repository.
Wrapper sourceRepository and dependencies
- Value exports
NativeSelectNativeSelectOptGroupNativeSelectOption
- Type exports
- None.
Setup, styles, and dependencies
These are React wrappers in the Thorbis design repository, not an installable npm UI package. In this repository, @/ resolves to src/ through tsconfig.json.
Use the repository's package.json and package-lock.json for dependencies. Load src/styles.css once at the application root for global tokens, typography, and themes. The wrappers compose colocated StyleX owners; include the generated token CSS and extracted StyleX output.
For another project, bring the wrapper and its transitive local imports, configure the alias, install the dependencies those files use, and provide the same token and style setup. Review the source before copying; the direct imports below are not a complete dependency closure.
Interactive examples require client-side React. This TanStack Start site server-renders and hydrates React; another host must hydrate or render the component on the client. Example code is the same TypeScript source used by its live preview.
Direct imports in this wrapper:
./field-target.stylex./select.stylex@stylexjs/stylexreact
Native selection, validation and reset
Choose a status using native option elements, optgroups and change events. NativeSelect accepts standard select attributes, including multiple and size; its ref points to the native control. This example uses defaultValue and the browser's form reset rather than duplicate selection state. A named control participates in FormData; a disabled control is omitted. The noValidate form owns visible validation and focuses the empty control. The example stays disabled with guidance until hydration and only changes its local preview. Prefer NativeSelect for a short simple list; use Select when grouped descriptions require a custom popup, and Combobox for searchable selection.
Read the complete example
import { exampleLayout } from "@/components/components/examples/example-layout.stylex";
import { useEffect, useId, useRef, useState, type FormEvent } from "react";
import { Button } from "@/components/ui/button";
import { FormActions, FormError, FormField, FormHelp, FormLabel } from "@/components/ui/form";
import { NativeSelect, NativeSelectOption } from "@/components/ui/native-select";
export function JobNativeSelectExample() {
const id = useId();
const select = useRef<HTMLSelectElement>(null);
const [ready, setReady] = useState(false);
const [error, setError] = useState(false);
const [preview, setPreview] = useState("");
useEffect(() => setReady(true), []);
function submit(event: FormEvent<HTMLFormElement>) {
event.preventDefault();
const value = new FormData(event.currentTarget).get("job-status");
if (!value) {
setError(true);
setPreview("");
select.current?.focus();
return;
}
setError(false);
setPreview(
`Preview status: ${select.current?.selectedOptions[0].label}. Nothing was saved or sent.`,
);
}
return (
<form
aria-label="Native job status example"
aria-busy={!ready}
{...exampleLayout.stack}
noValidate
onSubmit={submit}
onReset={() => {
setError(false);
setPreview("");
}}
>
<FormField>
<FormLabel htmlFor={`${id}-status`}>Job status</FormLabel>
<NativeSelect
ref={select}
id={`${id}-status`}
name="job-status"
defaultValue=""
required
disabled={!ready}
aria-invalid={error || undefined}
aria-describedby={`${id}-help`}
onChange={(event) => {
setError(!event.currentTarget.value);
setPreview("");
}}
>
<NativeSelectOption value="" disabled>
Choose a status
</NativeSelectOption>
<optgroup label="Active">
<NativeSelectOption value="scheduled">Scheduled</NativeSelectOption>
<NativeSelectOption value="en-route">En route</NativeSelectOption>
<NativeSelectOption value="on-site">On site</NativeSelectOption>
</optgroup>
<optgroup label="Finished">
<NativeSelectOption value="complete">Complete</NativeSelectOption>
<NativeSelectOption value="cancelled" disabled>
Cancelled — unavailable in this example
</NativeSelectOption>
</optgroup>
</NativeSelect>
{error ? (
<FormError id={`${id}-help`}>Choose an available job status.</FormError>
) : (
<FormHelp id={`${id}-help`}>
{ready
? "A short, stable list uses the platform's selection behavior. Native change events and form reset work normally."
: "The interactive example is loading. If it remains unavailable, reload the page with JavaScript enabled."}
</FormHelp>
)}
</FormField>
<FormField>
<FormLabel htmlFor={`${id}-unavailable`}>Unavailable status</FormLabel>
<NativeSelect
id={`${id}-unavailable`}
name="unavailable-status"
defaultValue=""
disabled
aria-describedby={`${id}-unavailable-help`}
>
<NativeSelectOption value="">No available statuses</NativeSelectOption>
</NativeSelect>
<FormHelp id={`${id}-unavailable-help`}>
A disabled field explains why it is unavailable and is omitted from form data.
</FormHelp>
</FormField>
<FormActions>
<Button type="submit" disabled={!ready}>
Preview status
</Button>
<Button type="reset" variant="ghost" disabled={!ready}>
Reset example
</Button>
</FormActions>
<p role="status" aria-live="polite">
{preview || "This example changes only its local preview."}
</p>
</form>
);
}
Adoption contract
- Start from the ShadCN accessible source primitive; keep upstream keyboard and focus behavior.
- Use Thorbis color, type, space, radius, shadow, and motion tokens only.
- Design hover, focus, selected, disabled, loading, empty, error, offline, and reduced-motion states where they apply.
- Verify desktop and 390px mobile, keyboard order, accessible names, and stable async geometry.