Radio Group
Pozwala wybrać dokładnie jedną opcję z niewielkiego zestawu, pokazując wszystkie opcje naraz.
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
- 1KontrolkaOkrąg. Jego obramowanie ma kontrast 3:1 względem strony, a wokół niego jest niewidoczny cel o wielkości 24 px.
- 2EtykietaNazywa opcję. Kliknięcie w nią wybiera opcję.
- 3WskaźnikKropka wybranej opcji. Pojawia się ze sprężystą animacją.
Instalacja
$ pnpm dlx shadcn@latest add https://www.prfct.dev/r/radio-group.jsonUż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.
Tylko zaproszeni członkowie mogą zobaczyć ten projekt i do niego dołączyć.
Każdy w twojej organizacji może go znaleźć i wyświetlić.
Każdy w internecie może go wyświetlić. Edytować mogą tylko członkowie.
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.
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ą.
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ł
Zapisz się do newslettera
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.
| Klawisz | Dział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+FieldLegendalboaria-label/aria-labelledbynaRadioGroup, aby pytanie było odczytywane razem z każdą opcją. - Nadaj etykietę każdemu przyciskowi — przez
FieldLabelwskazujący jegoid. 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.
RadioGroupItem
Renderuje <span role="radio"> z ukrytym polem input. Przyjmuje wszystkie propsy Radio.Root z Base UI.