Przejdź do treści

Native Select

Natywny element select przeglądarki, dopasowany wyglądem do prfct — najlepszy na urządzeniach mobilnych, w długich formularzach i wszędzie tam, gdzie kontrolka ma działać bez JavaScriptu.

Kod źródłowyRejestr
import { Field, FieldLabel } from "@/components/ui/field"
import { NativeSelect, NativeSelectOption } from "@/components/ui/native-select"

export default function NativeSelectDemo() {
  return (
    <Field className="w-64">
      <FieldLabel htmlFor="native-select-demo">Okres rozliczeniowy</FieldLabel>
      <NativeSelect
        id="native-select-demo"
        defaultValue="yearly"
        className="w-full"
      >
        <NativeSelectOption value="monthly">Miesięcznie</NativeSelectOption>
        <NativeSelectOption value="quarterly">Kwartalnie</NativeSelectOption>
        <NativeSelectOption value="yearly">
          Rocznie · 20% taniej
        </NativeSelectOption>
      </NativeSelect>
    </Field>
  )
}

Instalacja

$ pnpm dlx shadcn@latest add https://www.prfct.dev/r/native-select.json

Użycie

import {
  NativeSelect,
  NativeSelectOptGroup,
  NativeSelectOption,
} from "@/components/ui/native-select"
<NativeSelect defaultValue="yearly">
  <NativeSelectOption value="monthly">Monthly</NativeSelectOption>
  <NativeSelectOption value="yearly">Yearly</NativeSelectOption>
</NativeSelect>

className stylizuje kontener, który domyślnie ma w-fit — aby ustalić szerokość kontrolki, przekaż np. w-full. Wszystkie pozostałe propsy trafiają do elementu <select>.

Przykłady

Rozmiary

Rozmiary sm, default i lg mają te same wysokości co Input i Button, więc filtr i powiązana z nim akcja układają się w jednej linii.

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

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

export default function NativeSelectSizes() {
  return (
    <div className="flex flex-col gap-4">
      {sizes.map((size) => (
        <div key={size} className="flex items-center gap-2">
          <NativeSelect
            size={size}
            defaultValue="7"
            aria-label={`Zakres dat (${size})`}
            className="w-48"
          >
            <NativeSelectOption value="1">Ostatnie 24 godziny</NativeSelectOption>
            <NativeSelectOption value="7">Ostatnie 7 dni</NativeSelectOption>
            <NativeSelectOption value="30">Ostatnie 30 dni</NativeSelectOption>
          </NativeSelect>
          <Button size={size} variant="outline">
            Eksportuj
          </Button>
        </div>
      ))}
    </div>
  )
}

Grupy opcji

NativeSelectOptGroup dodaje do długich list nagłówki, których nie da się wybrać. Systemy operacyjne renderują je natywnie — z wcięciem na komputerach, jako osobne sekcje w mobilnych oknach wyboru.

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

const zones = {
  Americas: [
    ["America/New_York", "Nowy Jork (UTC−4)"],
    ["America/Chicago", "Chicago (UTC−5)"],
    ["America/Los_Angeles", "Los Angeles (UTC−7)"],
  ],
  Europe: [
    ["Europe/London", "Londyn (UTC+1)"],
    ["Europe/Warsaw", "Warszawa (UTC+2)"],
    ["Europe/Helsinki", "Helsinki (UTC+3)"],
  ],
  "Asia Pacific": [
    ["Asia/Singapore", "Singapur (UTC+8)"],
    ["Asia/Tokyo", "Tokio (UTC+9)"],
    ["Australia/Sydney", "Sydney (UTC+10)"],
  ],
}

export default function NativeSelectGroups() {
  return (
    <Field className="w-72">
      <FieldLabel htmlFor="native-select-zone">Strefa czasowa</FieldLabel>
      <NativeSelect
        id="native-select-zone"
        defaultValue="Europe/Warsaw"
        className="w-full"
      >
        {Object.entries(zones).map(([region, options]) => (
          <NativeSelectOptGroup key={region} label={region}>
            {options.map(([value, label]) => (
              <NativeSelectOption key={value} value={value}>
                {label}
              </NativeSelectOption>
            ))}
          </NativeSelectOptGroup>
        ))}
      </NativeSelect>
      <FieldDescription>
        Używana w zaplanowanych raportach i przypomnieniach.
      </FieldDescription>
    </Field>
  )
}

Nieprawidłowy i wyłączony

Pusta, wyłączona pierwsza opcja działa jak placeholder: w połączeniu z required przeglądarka nie wyśle formularza, dopóki ktoś nie dokona prawdziwego wyboru. Nieprawidłowy wybór oznacz atrybutem aria-invalid i komunikatem FieldError — dokładnie tak jak w polu tekstowym.

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

export default function NativeSelectStates() {
  return (
    <FieldGroup className="grid w-full max-w-xl gap-5 sm:grid-cols-2">
      <Field data-invalid>
        <FieldLabel htmlFor="native-select-invalid">Kraj</FieldLabel>
        <NativeSelect
          id="native-select-invalid"
          defaultValue=""
          required
          aria-invalid
          aria-describedby="native-select-invalid-error"
          className="w-full"
        >
          <NativeSelectOption value="" disabled>
            Wybierz kraj
          </NativeSelectOption>
          <NativeSelectOption value="pl">Polska</NativeSelectOption>
          <NativeSelectOption value="de">Niemcy</NativeSelectOption>
        </NativeSelect>
        <FieldError id="native-select-invalid-error">
          Wybierz kraj, w którym płacisz podatki.
        </FieldError>
      </Field>
      <Field data-disabled>
        <FieldLabel htmlFor="native-select-disabled">Waluta</FieldLabel>
        <NativeSelect
          id="native-select-disabled"
          defaultValue="eur"
          disabled
          className="w-full"
        >
          <NativeSelectOption value="eur">Euro (EUR)</NativeSelectOption>
        </NativeSelect>
      </Field>
    </FieldGroup>
  )
}

Wytyczne

Kiedy używać

  • W procesach używanych głównie na urządzeniach mobilnych: telefon otwiera wtedy systemowe okno wyboru, które jest szybsze i dobrze znane.
  • W długich formularzach i na stronach renderowanych na serwerze, które muszą działać, zanim załaduje się JavaScript — albo całkiem bez niego.
  • W prostych listach opcji tekstowych, w których własne renderowanie niczego nie wnosi.

Kiedy nie używać

  • Gdy opcje potrzebują ikon, opisów lub własnego układu — użyj Select.
  • Gdy lista jest na tyle długa, że przydaje się wyszukiwanie — użyj Combobox.
  • Gdy od dwóch do pięciu opcji trzeba porównać jednym spojrzeniem — użyj Radio Group lub Toggle Group.

Natywny czy własny?

KwestiaNative SelectSelect
Na telefonieSystemowe okno wyboruPopup prfct
Działa przed hydratacjąTakNie
Bogata treść opcjiTylko tekstIkony, opisy, dowolny JSX
Spójny wygląd na wszystkich platformachTylko wyzwalaczWyzwalacz i lista
Klawiatura i wybór przez wpisywanieNatywnieZapewnia Base UI

Wybierz jedno rozwiązanie dla danego obszaru produktu i trzymaj się go konsekwentnie — oba naraz w jednym formularzu sprawiają wrażenie niedokończonej pracy.

Dostępność

Native Select to prawdziwy <select>, więc bez jednej linijki skryptu przejmuje z platformy obsługę klawiatury, semantykę dla czytników ekranu i zachowanie w formularzach.

  • Nadaj mu etykietę — przez FieldLabel z htmlFor/id albo, w kompaktowych filtrach, przez aria-label.
  • Kolory systemowe. Opcje używają kolorów systemowych Canvas i CanvasText, więc w trybie ciemnym rozwinięta lista podąża za systemem operacyjnym i za color-scheme strony.
  • Kontrast. Obramowanie korzysta z tokenu input o kontraście 3:1 względem strony; ikona strzałki jest dekoracyjna i ukryta przed technologiami wspomagającymi.
KlawiszDziałanie
Tab
Przenosi fokus na listę wyboru.
SpacjaEnterAlt↓
Rozwija listę (zależnie od platformy).
↑↓
Zmienia wybór — w większości przeglądarek na komputerach bez rozwijania listy.
A–Z
Przeskakuje do następnej opcji zaczynającej się na tę literę.

Dokumentacja API

NativeSelect

Renderuje kontener <div> z elementem <select> i ikoną strzałki. Przyjmuje wszystkie natywne propsy elementu select.

PropTypDomyślnie
size

Wysokość i wewnętrzne odstępy: 32, 36 lub 40 px.

"sm" | "default" | "lg""default"
className

Trafia do kontenera. Służy do ustawiania szerokości i układu.

stringBrak wartości domyślnej
value

Wartość kontrolowana. Używaj razem z onChange.

stringBrak wartości domyślnej
defaultValue

Początkowa wartość listy niekontrolowanej.

stringBrak wartości domyślnej
disabled

Wyłącza listę i ją przygasza.

booleanfalse
aria-invalid

Pokazuje obramowanie i poświatę fokusu w stanie błędu.

booleanBrak wartości domyślnej

NativeSelectOption

Renderuje element <option>. Przyjmuje wszystkie natywne propsy opcji, w tym value i disabled.

NativeSelectOptGroup

Renderuje element <optgroup>. W label podaj nagłówek grupy.