Przejdź do treści

Radio Group

Pozwala wybrać dokładnie jedną opcję z niewielkiego zestawu, pokazując wszystkie opcje naraz.

Kod źródłowyBase UIRejestr
import { Field, FieldLabel } from "@/components/ui/field"
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"

const frequencies = [
  { value: "realtime", label: "Na bieżąco" },
  { value: "daily", label: "Codzienne podsumowanie" },
  { value: "weekly", label: "Cotygodniowe podsumowanie" },
]

export default function RadioGroupDemo() {
  return (
    <RadioGroup
      defaultValue="daily"
      aria-label="Częstotliwość powiadomień"
      className="w-52"
    >
      {frequencies.map((frequency) => (
        <Field key={frequency.value} orientation="horizontal">
          <RadioGroupItem
            value={frequency.value}
            id={`frequency-${frequency.value}`}
          />
          <FieldLabel
            htmlFor={`frequency-${frequency.value}`}
            className="font-normal"
          >
            {frequency.label}
          </FieldLabel>
        </Field>
      ))}
    </RadioGroup>
  )
}

Anatomia

  1. 1KontrolkaOkrąg. Jego obramowanie ma kontrast 3:1 względem strony, a wokół niego jest niewidoczny cel o wielkości 24 px.
  2. 2EtykietaNazywa opcję. Kliknięcie w nią wybiera opcję.
  3. 3WskaźnikKropka wybranej opcji. Pojawia się ze sprężystą animacją.

Instalacja

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

Użycie

import { Field, FieldLabel } from "@/components/ui/field"
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"
<RadioGroup defaultValue="daily">
  <Field orientation="horizontal">
    <RadioGroupItem value="daily" id="daily" />
    <FieldLabel htmlFor="daily">Daily summary</FieldLabel>
  </Field>
  <Field orientation="horizontal">
    <RadioGroupItem value="weekly" id="weekly" />
    <FieldLabel htmlFor="weekly">Weekly summary</FieldLabel>
  </Field>
</RadioGroup>

Zaznaczony przycisk radiowy wypełnia się kolorem marki, a jego kropka sprężyście wskakuje na miejsce.

Przykłady

Z opisami

Gdy opcje wymagają wyjaśnienia, dodaj do każdej etykiety opis w FieldContent. Użytkownicy porównują opcje, czytając opisy jeden obok drugiego — to jeden z głównych powodów, by wybrać przyciski radiowe zamiast listy wyboru.

import {
  Field,
  FieldContent,
  FieldDescription,
  FieldLabel,
} from "@/components/ui/field"
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"

const visibility = [
  {
    value: "private",
    label: "Prywatny",
    description: "Tylko zaproszeni członkowie mogą zobaczyć ten projekt i do niego dołączyć.",
  },
  {
    value: "internal",
    label: "Wewnętrzny",
    description: "Każdy w twojej organizacji może go znaleźć i wyświetlić.",
  },
  {
    value: "public",
    label: "Publiczny",
    description: "Każdy w internecie może go wyświetlić. Edytować mogą tylko członkowie.",
  },
]

export default function RadioGroupDescriptions() {
  return (
    <RadioGroup
      defaultValue="internal"
      aria-label="Widoczność projektu"
      className="max-w-sm gap-4"
    >
      {visibility.map((option) => (
        <Field key={option.value} orientation="horizontal">
          <RadioGroupItem
            value={option.value}
            id={`visibility-${option.value}`}
          />
          <FieldContent>
            <FieldLabel htmlFor={`visibility-${option.value}`}>
              {option.label}
            </FieldLabel>
            <FieldDescription>{option.description}</FieldDescription>
          </FieldContent>
        </Field>
      ))}
    </RadioGroup>
  )
}

Karty wyboru

Opakuj każde pole w FieldLabel, aby cała karta była klikalna. Karty sprawdzają się przy decyzjach, za którymi stoi cena, limit lub lista funkcji — wyborze planu, szybkości dostawy, środowiska wdrożenia.

import {
  Field,
  FieldContent,
  FieldDescription,
  FieldLabel,
  FieldTitle,
} from "@/components/ui/field"
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"

const plans = [
  {
    value: "hobby",
    title: "Hobby",
    description: "1 projekt · Wsparcie społeczności",
    price: "0 $",
  },
  {
    value: "pro",
    title: "Pro",
    description: "Bez limitu projektów · Wsparcie e-mailowe",
    price: "20 $",
  },
  {
    value: "team",
    title: "Team",
    description: "Role, SSO i dzienniki audytu",
    price: "60 $",
  },
]

export default function RadioGroupChoiceCards() {
  return (
    <RadioGroup
      defaultValue="pro"
      aria-label="Plan"
      className="w-full max-w-md"
    >
      {plans.map((plan) => (
        <FieldLabel key={plan.value} htmlFor={`plan-${plan.value}`}>
          <Field orientation="horizontal">
            <RadioGroupItem value={plan.value} id={`plan-${plan.value}`} />
            <FieldContent>
              <FieldTitle>{plan.title}</FieldTitle>
              <FieldDescription>{plan.description}</FieldDescription>
            </FieldContent>
            <span className="text-sm font-semibold text-foreground tabular-nums">
              {plan.price}
              <span className="font-normal text-muted-foreground">/mies.</span>
            </span>
          </Field>
        </FieldLabel>
      ))}
    </RadioGroup>
  )
}

Układ poziomy

Krótkie opcje ułóż w rzędzie, gdy tworzą skalę albo zestaw równorzędnych wariantów. Nazwij grupę za pomocą FieldSet i FieldLegend — rząd przycisków radiowych bez pytania jest niejednoznaczny.

Gęstość tabeli
import { Field, FieldLabel, FieldLegend, FieldSet } from "@/components/ui/field"
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"

const densities = [
  { value: "compact", label: "Kompaktowa" },
  { value: "default", label: "Domyślna" },
  { value: "comfortable", label: "Wygodna" },
]

export default function RadioGroupHorizontal() {
  return (
    <FieldSet className="w-fit">
      <FieldLegend variant="label">Gęstość tabeli</FieldLegend>
      <RadioGroup
        defaultValue="default"
        className="flex flex-wrap gap-x-6 gap-y-3"
      >
        {densities.map((density) => (
          <Field
            key={density.value}
            orientation="horizontal"
            className="w-auto"
          >
            <RadioGroupItem
              value={density.value}
              id={`density-${density.value}`}
            />
            <FieldLabel
              htmlFor={`density-${density.value}`}
              className="font-normal"
            >
              {density.label}
            </FieldLabel>
          </Field>
        ))}
      </RadioGroup>
    </FieldSet>
  )
}

Stany

Wyłącz grupę, gdy wybór jeszcze nie ma zastosowania, i obok wyjaśnij dlaczego. Do walidacji ustaw required, oznacz elementy przez aria-invalid i pokaż FieldError pod grupą.

Okres rozliczeniowy
Szybkość dostawy
import {
  Field,
  FieldError,
  FieldLabel,
  FieldLegend,
  FieldSet,
} from "@/components/ui/field"
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"

export default function RadioGroupStates() {
  return (
    <div className="grid w-full max-w-lg gap-10 sm:grid-cols-2">
      <FieldSet>
        <FieldLegend variant="label">Okres rozliczeniowy</FieldLegend>
        <RadioGroup defaultValue="monthly" disabled>
          <Field orientation="horizontal" data-disabled>
            <RadioGroupItem value="monthly" id="cycle-monthly" />
            <FieldLabel htmlFor="cycle-monthly" className="font-normal">
              Miesięczny
            </FieldLabel>
          </Field>
          <Field orientation="horizontal" data-disabled>
            <RadioGroupItem value="yearly" id="cycle-yearly" />
            <FieldLabel htmlFor="cycle-yearly" className="font-normal">
              Roczny
            </FieldLabel>
          </Field>
        </RadioGroup>
      </FieldSet>
      <FieldSet data-invalid>
        <FieldLegend variant="label">Szybkość dostawy</FieldLegend>
        <RadioGroup aria-invalid required>
          <Field orientation="horizontal">
            <RadioGroupItem value="standard" id="ship-standard" aria-invalid />
            <FieldLabel htmlFor="ship-standard" className="font-normal">
              Standardowa (3–5 dni)
            </FieldLabel>
          </Field>
          <Field orientation="horizontal">
            <RadioGroupItem value="express" id="ship-express" aria-invalid />
            <FieldLabel htmlFor="ship-express" className="font-normal">
              Ekspresowa (1–2 dni)
            </FieldLabel>
          </Field>
        </RadioGroup>
        <FieldError>Wybierz szybkość dostawy.</FieldError>
      </FieldSet>
    </div>
  )
}

Wytyczne

Kiedy używać

  • Do wyboru dokładnie jednej opcji spośród dwóch do mniej więcej siedmiu.
  • Gdy warto widzieć i porównywać wszystkie opcje jednocześnie.

Kiedy nie używać

  • Do wyboru kilku opcji — użyj Checkbox.
  • Do pojedynczego ustawienia typu włącz/wyłącz — użyj Switch lub pola wyboru.
  • Gdy opcji jest więcej niż około siedmiu albo brakuje miejsca — użyj Select.
  • Do kompaktowych, segmentowych wyborów na paskach narzędzi (tryby widoku, wyrównanie) — użyj Toggle Group.

Domyślny wybór

Zaznacz z góry najczęściej wybieraną lub najbezpieczniejszą opcję — gdy ktoś już dokona wyboru, grupy przycisków radiowych nie da się przywrócić do stanu „nic nie zaznaczono”, więc pusta grupa ma sens tylko wtedy, gdy wybór musi być świadomy. W takim przypadku go waliduj.

Standardowa — bezpłatna

Ekspresowa — 12 zł

DobrzeRozsądna wartość domyślna — zmienia się ją tylko w razie potrzeby.

Zapisz się do newslettera

ŹlePojedynczego przycisku radiowego nie da się odznaczyć — użyj zamiast niego pola wyboru.

Kolejność

Układaj opcje logicznie — według wielkości, czasu lub prawdopodobieństwa — i trzymaj listę w pionie, chyba że opcje są bardzo krótkie. Wyrównaj przyciski radiowe do lewej, aby etykiety tworzyły równą kolumnę, którą łatwo przebiec wzrokiem.

Dostępność

RadioGroup renderuje role="radiogroup", a każdy element — role="radio" z aria-checked. Grupa zajmuje jedno miejsce w kolejności tabulacji, a fokus wędruje w jej obrębie (roving focus), zgodnie ze wzorcem ARIA dla grupy przycisków radiowych.

KlawiszDziałanie
Tab
Przenosi fokus do grupy — na zaznaczony przycisk albo na pierwszy, jeśli żaden nie jest zaznaczony — i z niej na zewnątrz.
↓→
Przechodzi do następnego przycisku i go zaznacza; po ostatnim wraca na początek.
↑←
Przechodzi do poprzedniego przycisku i go zaznacza; przed pierwszym przeskakuje na koniec.
Spacja
Zaznacza przycisk z fokusem, jeśli nie jest jeszcze zaznaczony.
  • Nazwij grupę. Użyj FieldSet + FieldLegend albo aria-label / aria-labelledby na RadioGroup, aby pytanie było odczytywane razem z każdą opcją.
  • Nadaj etykietę każdemu przyciskowi — przez FieldLabel wskazujący jego id. Base UI łączy etykietę z widocznym przyciskiem, więc kliknięcie tekstu go zaznacza.
  • Krawędzie i obszary dotyku. Obramowanie przycisków radiowych korzysta z tokenu input (3:1, WCAG 1.4.11), a wokół okręgu o średnicy 16 px jest obszar kliknięcia 24 × 24 px.

Dokumentacja API

RadioGroup

Renderuje <div role="radiogroup">. Przyjmuje wszystkie propsy RadioGroup z Base UI.

PropTypDomyślnie
value

Zaznaczona wartość. W trybie kontrolowanym używaj razem z onValueChange.

ValueBrak wartości domyślnej
defaultValue

Wartość zaznaczona na starcie, gdy grupa jest niekontrolowana.

ValueBrak wartości domyślnej
onValueChange

Wywoływana, gdy zmienia się zaznaczenie.

(value, details) => voidBrak wartości domyślnej
name

Pozwala wysłać zaznaczoną wartość razem z formularzem.

stringBrak wartości domyślnej
required

Wymaga wyboru przed wysłaniem formularza.

booleanfalse
disabled

Wyłącza wszystkie przyciski w grupie.

booleanfalse
readOnly

Pokazuje zaznaczenie, ale nie pozwala go zmienić.

booleanfalse

RadioGroupItem

Renderuje <span role="radio"> z ukrytym polem input. Przyjmuje wszystkie propsy Radio.Root z Base UI.

PropTypDomyślnie
valuewymagany

Wartość, którą reprezentuje ten przycisk.

ValueBrak wartości domyślnej
disabled

Wyłącza tylko ten przycisk.

booleanfalse
id

Pozwala wskazać przycisk z FieldLabel przez htmlFor.

stringBrak wartości domyślnej
aria-invalid

Pokazuje stan błędu.

booleanBrak wartości domyślnej