Forms
NativeSelect
A labeled native <select> on the Field contract: pass label, options (or <option> children) and error. Phones open the operating system's own picker (a wheel on iOS, a list on Android), which is quick for long plain-text lists such as countries, and works before JavaScript loads.
In Spoonjoy
Spoonjoy, a recipe app, is built with Ouro HX (how).
In its source: app/routes/cookbooks.$id.tsx · app/routes/shopping-list.tsx
Install
Once per app, with Tailwind CSS v4 and an @/* path alias (Ouro HX is built on Base UI, so --base base; skip -t in an existing app):
npx shadcn@latest init --base base --preset nova --yes --no-monoreponpx shadcn@latest add ourostack/ouro-ui/ouro-base#v1.2.13Then the component. From v0.8.0 it installs only the helpers it imports and never touches your stylesheet (Installing):
npx shadcn@latest add ourostack/ouro-ui/native-select#v1.2.13Or, with the @ouro namespace set up:
npx shadcn@latest add @ouro/ouro-base @ouro/native-selectIts tests, stories and Playwright spec are optional. They need Vitest 4.1.11 or later, and from v0.8.0 never change a version your app has (Testing):
npx shadcn@latest add ourostack/ouro-ui/native-select#v1.2.13 ourostack/ouro-ui/native-select-tests#v1.2.13import { NativeSelect } from "@/components/ouro/select/native-select";Paths use shadcn's default @/ alias; the CLI rewrites them to yours.
Example
<NativeSelect
label="Country"
name="country"
autoComplete="country"
placeholder="Choose a country"
options={[{ value: "nz", label: "New Zealand" }, { value: "za", label: "South Africa" }]}
error={errors?.country}
/>Use instead when
- Use
Selectinstead for a styled list (icons, groups with headings, several values withmultiple), andRadioGroupfor two to five choices that should all show.
Props
Type: NativeSelectProps.
children- Type:
ReactNode <option>and<optgroup>elements, when you do not passoptions.className- Type:
string - Layout only: margins, width, grid placement (
mt-4,col-span-2). Appearance classes warn in development; style fields in your theme. description- Type:
ReactNode - Supporting text under the label, read after the label through
aria-describedby. disabled- Type:
boolean - Disables the select and fades it.
error- Type:
ReadonlyErrorMessages - What is wrong, from your server action or client check. The message (or several, as a list) shows under the select, which gets
aria-invalid. A message is text or a React node (one with a link); an empty string or array means valid. hideLabel- Type:
boolean - Hides the label visually; screen readers still read it as the select's name.
label(required)- Type:
string - What the select is for, shown above it and read as its name. Required: every control needs a name. To hide it visually, keep it and add
hideLabel. name- Type:
string - The form field name the value submits under.
onChange- Type:
SelectHTMLAttributes<HTMLSelectElement>["onChange"] - Called with the change event when the user picks an option.
onValueChangegets the value. options- Type:
ReadonlyArray<NativeSelectOption | NativeSelectOptionGroup> - The choices:
[{ value: "nz", label: "New Zealand" }], and groups as{ label: "Oceania", options: [...] }. Or pass<option>children. placeholder- Type:
string - An empty first option shown, muted, while nothing is chosen:
"Choose a country". Withrequiredit cannot be chosen back, so the browser asks for a real choice. ref- Type:
Ref<HTMLSelectElement> - The select element.
required- Type:
boolean - A choice is required: the form will not submit without one, and the label gets a marker.
Native attributes
Also accepts the attributes in React's HTMLAttributes and SelectHTMLAttributes (id, aria-*, event handlers), passed to the rendered element, except those named on this page.
Don't use
These are not props. Passing one fails the type check with the fix in the error, and in development a console message names the fix.
errorMessage: Not a prop. Ouro UI useserror:<NativeSelect error="Required.">.helperText: Not a prop. Ouro UI usesdescription:<NativeSelect description="...">.invalid: Not a prop.errormarks the select invalid:<NativeSelect error="What is wrong">.isDisabled: Not a prop. Ouro UI usesdisabled:<NativeSelect disabled>.isInvalid: Not a prop.errormarks the select invalid:<NativeSelect error="What is wrong">.isMulti: Not a prop. For several values use<Select multiple>.isRequired: Not a prop. Ouro UI usesrequired:<NativeSelect required>.multiple: Not supported by NativeSelect. A native multiple select is hard to use; use<Select multiple>.
className is for layout only
Pass margins, width and grid placement (mt-4, w-full, col-span-2). Your classes always win over Ouro UI's: NativeSelect merges className last with cn (tailwind-merge), so a class that conflicts with its own replaces it.layout-classes
Appearance comes from props, not classes. In development, a class that sets appearance logs one warning per component instance, naming the prop to use instead. A class sets appearance when, after its variants (hover:, sm:) and any ! are removed, it starts with bg-, text-, border-, rounded-, shadow-, font-, ring-, from-, via-, to-, fill- or stroke-, or is border, rounded, shadow or ring, or has a dark: variant. Text wrapping, overflow and table layout classes (text-wrap, text-ellipsis, border-collapse) are layout and do not warn.
Alignment classes (text-center) warn too: set alignment on a parent element.
Types
ErrorMessage
One error message: text, or a React element for a message that carries a link or other markup.
type ErrorMessage = string | ReactElement;NativeSelectOption
One choice: the value it submits and the label it shows.
interface NativeSelectOption {
/** What the form submits. */
value: string;
/** What the picker shows. */
label: string;
/** The option shows but cannot be chosen. */
disabled?: boolean | undefined;
}NativeSelectOptionGroup
A labeled group of options, an <optgroup>.
interface NativeSelectOptionGroup {
/** The group's heading. */
label: string;
/** The options in the group. */
options: NativeSelectOption[];
}ReadonlyErrorMessages
A read-only message view accepted by fields and Form; mutable input arrays remain valid.
type ReadonlyErrorMessages = ErrorMessage | readonly ErrorMessage[];Claim sources
- layout-classes: test:registry/ouro/components/ouro/select/native-select.test.tsx::NativeSelect > is disabled on request, forwards a ref and lets layout classes win
verify:testExists(registry/ouro/components/ouro/select/native-select.test.tsx::NativeSelect > is disabled on request, forwards a ref and lets layout classes win)