HextaUI

Native select

Le select natif du navigateur, stylé pour s’accorder à Input, avec un placeholder discret, la prise en charge de Field et le sélecteur du système sur chaque appareil.

import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import { NativeSelect, NativeSelectOption } from "@/components/ui/native-select"

export function NativeSelectDemo() {
  return (
    <div className="grid w-full max-w-sm gap-4">
      <div className="flex flex-col gap-2">
        <Label htmlFor="native-select-demo-name">Project name</Label>
        <Input id="native-select-demo-name" defaultValue="HextaUI" />
      </div>
      <div className="flex flex-col gap-2">
        <Label htmlFor="native-select-demo-framework">Framework</Label>
        <NativeSelect id="native-select-demo-framework" className="w-full">
          <NativeSelectOption value="">Select a framework</NativeSelectOption>
          <NativeSelectOption value="next">Next.js</NativeSelectOption>
          <NativeSelectOption value="remix">Remix</NativeSelectOption>
          <NativeSelectOption value="astro">Astro</NativeSelectOption>
          <NativeSelectOption value="vite">Vite</NativeSelectOption>
        </NativeSelect>
      </div>
    </div>
  )
}
pnpm dlx shadcn@latest add https://hextaui.com/r/native-select.json

Ajoute le composant, les tokens de thème HextaUI et les composants HextaUI dont il dépend.

import {
  NativeSelect,
  NativeSelectOptGroup,
  NativeSelectOption,
} from "@/components/ui/native-select"
<NativeSelect>
  <NativeSelectOption value="">Select a fruit</NativeSelectOption>
  <NativeSelectOption value="apple">Apple</NativeSelectOption>
  <NativeSelectOption value="banana">Banana</NativeSelectOption>
</NativeSelect>

Une option avec une value vide sert de placeholder : elle s'affiche en texte atténué tant que rien n'est choisi. Mettez className sur le select pour dimensionner son wrapper, par exemple w-full.

NativeSelect
├── NativeSelectOption
└── NativeSelectOptGroup
    └── NativeSelectOption

Groupes

<NativeSelectOptGroup /> organise les options sous des intitulés dans le sélecteur du système.

import {
  NativeSelect,
  NativeSelectOptGroup,
  NativeSelectOption,
} from "@/components/ui/native-select"

export function NativeSelectGroups() {
  return (
    <NativeSelect aria-label="Department">
      <NativeSelectOption value="">Select department</NativeSelectOption>
      <NativeSelectOptGroup label="Engineering">
        <NativeSelectOption value="frontend">Frontend</NativeSelectOption>
        <NativeSelectOption value="backend">Backend</NativeSelectOption>
        <NativeSelectOption value="devops">DevOps</NativeSelectOption>
      </NativeSelectOptGroup>
      <NativeSelectOptGroup label="Design">
        <NativeSelectOption value="product">Product design</NativeSelectOption>
        <NativeSelectOption value="brand">Brand</NativeSelectOption>
      </NativeSelectOptGroup>
      <NativeSelectOptGroup label="Operations">
        <NativeSelectOption value="support">
          Customer support
        </NativeSelectOption>
        <NativeSelectOption value="people">People</NativeSelectOption>
      </NativeSelectOptGroup>
    </NativeSelect>
  )
}

Tailles

Les tailles, bordures, anneaux de focus et styles invalides correspondent exactement à ceux d'Input, si bien que les deux s'alignent sur une même ligne.

import { Input } from "@/components/ui/input"
import { NativeSelect, NativeSelectOption } from "@/components/ui/native-select"

const sizes = ["sm", "default", "lg"] as const

export function NativeSelectSizes() {
  return (
    <div className="flex w-full max-w-md flex-col gap-3">
      {sizes.map((size) => (
        <div key={size} className="flex gap-2">
          <Input
            size={size}
            placeholder={`Input ${size}`}
            aria-label={`Input ${size}`}
          />
          <NativeSelect size={size} aria-label={`Select ${size}`}>
            <NativeSelectOption value="">{size}</NativeSelectOption>
            <NativeSelectOption value="a">Option A</NativeSelectOption>
          </NativeSelect>
        </div>
      ))}
    </div>
  )
}

Field

Dans un <Field />, le label et la description se relient d'eux-mêmes et les erreurs s'affichent quand la valeur change. Soumettez sans choisir de plan : le select devient rouge, tremble une fois, et le navigateur le désigne.

"use client"

import { Form } from "@base-ui/react/form"

import { Button } from "@/components/ui/button"
import {
  Field,
  FieldDescription,
  FieldError,
  FieldLabel,
} from "@/components/ui/field"
import { NativeSelect, NativeSelectOption } from "@/components/ui/native-select"

export function NativeSelectField() {
  return (
    <Form
      className="flex w-full max-w-sm flex-col gap-4"
      onSubmit={(event) => event.preventDefault()}
    >
      <Field validationMode="onChange">
        <FieldLabel>Plan</FieldLabel>
        <NativeSelect name="plan" required className="w-full">
          <NativeSelectOption value="">Choose a plan</NativeSelectOption>
          <NativeSelectOption value="hobby">Hobby</NativeSelectOption>
          <NativeSelectOption value="pro">Pro</NativeSelectOption>
          <NativeSelectOption value="team">Team</NativeSelectOption>
        </NativeSelect>
        <FieldDescription>You can change this later.</FieldDescription>
        <FieldError match="valueMissing">Choose a plan to continue.</FieldError>
      </Field>
      <Button type="submit" className="self-start">
        Continue
      </Button>
    </Form>
  )
}

Invalide

Définissez aria-invalid et reliez le message avec aria-describedby.

import { NativeSelect, NativeSelectOption } from "@/components/ui/native-select"

export function NativeSelectInvalid() {
  return (
    <div className="flex w-full max-w-sm flex-col gap-2">
      <NativeSelect
        aria-label="Country"
        aria-invalid
        aria-describedby="native-select-invalid-message"
        className="w-full"
      >
        <NativeSelectOption value="">Select a country</NativeSelectOption>
        <NativeSelectOption value="in">India</NativeSelectOption>
        <NativeSelectOption value="de">Germany</NativeSelectOption>
      </NativeSelect>
      <p
        id="native-select-invalid-message"
        className="text-sm text-destructive"
      >
        We don&apos;t ship to the selected region yet.
      </p>
    </div>
  )
}

Désactivé

Désactivez tout le select, ou seulement les options indisponibles.

import { NativeSelect, NativeSelectOption } from "@/components/ui/native-select"

export function NativeSelectDisabled() {
  return (
    <div className="flex flex-wrap items-center gap-3">
      <NativeSelect aria-label="Region" disabled defaultValue="eu">
        <NativeSelectOption value="us">United States</NativeSelectOption>
        <NativeSelectOption value="eu">Europe</NativeSelectOption>
      </NativeSelect>
      <NativeSelect aria-label="Plan" defaultValue="pro">
        <NativeSelectOption value="hobby">Hobby</NativeSelectOption>
        <NativeSelectOption value="pro">Pro</NativeSelectOption>
        <NativeSelectOption value="enterprise" disabled>
          Enterprise (contact sales)
        </NativeSelectOption>
      </NativeSelect>
    </div>
  )
}

Contrôlé

Passez value et onChange comme pour un select ordinaire.

"use client"

import * as React from "react"

import { NativeSelect, NativeSelectOption } from "@/components/ui/native-select"

const zones = [
  { value: "America/New_York", label: "New York" },
  { value: "Europe/London", label: "London" },
  { value: "Asia/Kolkata", label: "Kolkata" },
  { value: "Asia/Tokyo", label: "Tokyo" },
]

export function NativeSelectControlled() {
  const [zone, setZone] = React.useState("Asia/Kolkata")
  const time = new Intl.DateTimeFormat("en-US", {
    hour: "numeric",
    minute: "2-digit",
    timeZone: zone,
  }).format(new Date(Date.UTC(2026, 9, 4, 9, 0)))

  return (
    <div className="flex flex-col items-start gap-2">
      <NativeSelect
        aria-label="Time zone"
        value={zone}
        onChange={(event) => setZone(event.target.value)}
      >
        {zones.map((option) => (
          <NativeSelectOption key={option.value} value={option.value}>
            {option.label}
          </NativeSelectOption>
        ))}
      </NativeSelect>
      <p className="text-sm text-muted-foreground">
        09:00 UTC is {time} there.
      </p>
    </div>
  )
}

Contenu long

Les longs labels sélectionnés se terminent par des points de suspension au lieu de passer sous le chevron.

import { NativeSelect, NativeSelectOption } from "@/components/ui/native-select"

export function NativeSelectLongContent() {
  return (
    <div className="w-56 max-w-full">
      <NativeSelect
        aria-label="Billing account"
        defaultValue="long"
        className="w-full"
      >
        <NativeSelectOption value="short">Personal</NativeSelectOption>
        <NativeSelectOption value="long">
          The International Subsidiary Billing Account for Europe and Asia
        </NativeSelectOption>
      </NativeSelect>
    </div>
  )
}

De droite à gauche

Le chevron et le padding passent de l'autre côté.

import { NativeSelect, NativeSelectOption } from "@/components/ui/native-select"

export function NativeSelectRtl() {
  return (
    <div dir="rtl">
      <NativeSelect aria-label="اللغة">
        <NativeSelectOption value="">اختر لغة</NativeSelectOption>
        <NativeSelectOption value="ar">العربية</NativeSelectOption>
        <NativeSelectOption value="en">الإنجليزية</NativeSelectOption>
        <NativeSelectOption value="fr">الفرنسية</NativeSelectOption>
      </NativeSelect>
    </div>
  )
}
  • Utilisez Native select pour les listes simples, surtout sur mobile, où le sélecteur du système est la façon la plus rapide et la plus familière de choisir.
  • Utilisez Combobox quand il faut pouvoir rechercher, ou quand les options ont besoin d'icônes, de descriptions ou de mises en page personnalisées.
ToucheAction
TabDonne le focus au select.
SpaceEnterOuvre le sélecteur du système.
↑↓Modifie la sélection, ou se déplace dans le sélecteur ouvert.
A–ZSaute à l'option qui commence par le texte saisi.

<NativeSelect /> accepte tous les attributs de select. Les parties option acceptent les attributs de leur élément.

PropTypePar défaut
size
"sm" | "default" | "lg""default"
htmlSizeL’attribut size natif, renommé car size est la variante.
number–
shakeTremble une fois quand l'envoi d'un formulaire le trouve invalide.
booleantrue
classNameAppliqué au wrapper, il dimensionne donc tout le contrôle.
string–
value
string–
defaultValue
string–
disabled
booleanfalse
required
booleanfalse
AttributDescription
data-slot="native-select-wrapper"Le wrapper, avec data-size.
data-slot="native-select"L'élément select.
data-slot="native-select-icon"Le chevron. Il fonce au survol et au focus.
data-invalidPrésent quand un Field englobant est invalide.
AttributDescription
data-slot="native-select-option"Une option.
data-slot="native-select-optgroup"Un groupe d'options avec un label.

Utilisé dans les blocks

Des blocks qui s’appuient sur Native select.