Przejdź do treści

Checkbox

Włącza lub wyłącza pojedynczą opcję albo zaznacza dowolną liczbę opcji z zestawu — zmiana obowiązuje po wysłaniu formularza.

Kod źródłowyBase UIRejestr
import { Checkbox } from "@/components/ui/checkbox"
import { Field, FieldGroup, FieldLabel } from "@/components/ui/field"

export default function CheckboxDemo() {
  return (
    <FieldGroup className="w-64 gap-3">
      <Field orientation="horizontal">
        <Checkbox id="demo-mentions" defaultChecked />
        <FieldLabel htmlFor="demo-mentions" className="font-normal">
          Wzmianki i odpowiedzi
        </FieldLabel>
      </Field>
      <Field orientation="horizontal">
        <Checkbox id="demo-assigned" defaultChecked />
        <FieldLabel htmlFor="demo-assigned" className="font-normal">
          Zgłoszenia przypisane do mnie
        </FieldLabel>
      </Field>
      <Field orientation="horizontal">
        <Checkbox id="demo-digest" />
        <FieldLabel htmlFor="demo-digest" className="font-normal">
          Cotygodniowe podsumowanie
        </FieldLabel>
      </Field>
    </FieldGroup>
  )
}

Anatomia

  1. 1PoleKwadrat 16 px z niewidocznym obszarem kliknięcia 24 px. Zaznaczony wypełnia się kolorem marki i rysuje znacznik jak pociągnięcie pióra; w stanie pośrednim pokazuje kreskę.
  2. 2OpisOpcjonalne wyjaśnienie, co robi opcja — wyciszonym tekstem.
  3. 3EtykietaNazywa opcję. Kliknięcie etykiety przełącza pole.

Instalacja

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

Użycie

import { Checkbox } from "@/components/ui/checkbox"
import { Field, FieldLabel } from "@/components/ui/field"
<Field orientation="horizontal">
  <Checkbox id="terms" />
  <FieldLabel htmlFor="terms">Accept terms and conditions</FieldLabel>
</Field>

Po zaznaczeniu pola znacznik rysuje się sam, jak pociągnięcie pióra. Pola zaznaczone i w stanie pośrednim wypełniają się kolorem marki — w prfct kolor oznacza stan i zaznaczenie, a czerń jest zarezerwowana dla akcji.

Przykłady

Z opisem

Umieść etykietę i opis w FieldContent obok pola wyboru. Opis wyjaśnia konsekwencje, a etykieta pozostaje na tyle krótka, żeby dało się ją szybko przejrzeć wzrokiem.

import { Checkbox } from "@/components/ui/checkbox"
import {
  Field,
  FieldContent,
  FieldDescription,
  FieldLabel,
} from "@/components/ui/field"

export default function CheckboxDescription() {
  return (
    <Field orientation="horizontal" className="max-w-sm">
      <Checkbox id="terms" defaultChecked />
      <FieldContent>
        <FieldLabel htmlFor="terms">Akceptuję regulamin</FieldLabel>
        <FieldDescription>
          Kontynuując, akceptujesz nasz Regulamin i potwierdzasz zapoznanie się z Polityką prywatności.
        </FieldDescription>
      </FieldContent>
    </Field>
  )
}

Stan pośredni

Nadrzędne pole wyboru, które steruje grupą, jest w stanie pośrednim, gdy zaznaczona jest tylko część pól podrzędnych. Ustaw indeterminate razem z checked; kliknięcie pola nadrzędnego zaznacza wtedy albo czyści wszystkie pola podrzędne. Dodaj polom podrzędnym wcięcie, żeby hierarchia była widoczna, a nie tylko sugerowana.

"use client"

import * as React from "react"

import { Checkbox } from "@/components/ui/checkbox"
import { Field, FieldGroup, FieldLabel } from "@/components/ui/field"

const permissions = [
  { id: "read", label: "Odczyt zawartości repozytorium" },
  { id: "write", label: "Wypychanie commitów i otwieranie pull requestów" },
  { id: "admin", label: "Zarządzanie ustawieniami i webhookami" },
]

export default function CheckboxIndeterminate() {
  const [granted, setGranted] = React.useState<string[]>(["read"])
  const all = granted.length === permissions.length
  const some = granted.length > 0 && !all

  return (
    <FieldGroup className="w-80 gap-3">
      <Field orientation="horizontal">
        <Checkbox
          id="permissions-all"
          checked={all}
          indeterminate={some}
          onCheckedChange={(checked) =>
            setGranted(checked ? permissions.map((p) => p.id) : [])
          }
        />
        <FieldLabel htmlFor="permissions-all">Wszystkie uprawnienia</FieldLabel>
      </Field>
      <div className="ml-2 flex flex-col gap-3 border-l pl-5">
        {permissions.map((permission) => (
          <Field key={permission.id} orientation="horizontal">
            <Checkbox
              id={`permission-${permission.id}`}
              checked={granted.includes(permission.id)}
              onCheckedChange={(checked) =>
                setGranted((current) =>
                  checked
                    ? [...current, permission.id]
                    : current.filter((id) => id !== permission.id)
                )
              }
            />
            <FieldLabel
              htmlFor={`permission-${permission.id}`}
              className="font-normal"
            >
              {permission.label}
            </FieldLabel>
          </Field>
        ))}
      </div>
    </FieldGroup>
  )
}

Stany

Wyłączone pola wyboru blakną do połowy krycia i ignorują interakcję. Przy błędzie oznacz pole wyboru atrybutem aria-invalid, a otaczający je Field — atrybutem data-invalid; w FieldError wyjaśnij, co zrobić — sama czerwona ramka nie wystarczy.

import { Checkbox } from "@/components/ui/checkbox"
import {
  Field,
  FieldContent,
  FieldError,
  FieldGroup,
  FieldLabel,
} from "@/components/ui/field"

export default function CheckboxStates() {
  return (
    <FieldGroup className="w-80 gap-4">
      <Field orientation="horizontal" data-disabled>
        <Checkbox id="state-disabled" disabled />
        <FieldLabel htmlFor="state-disabled" className="font-normal">
          Wyłączone
        </FieldLabel>
      </Field>
      <Field orientation="horizontal" data-disabled>
        <Checkbox id="state-disabled-checked" disabled defaultChecked />
        <FieldLabel htmlFor="state-disabled-checked" className="font-normal">
          Wyłączone i zaznaczone
        </FieldLabel>
      </Field>
      <Field orientation="horizontal" data-invalid>
        <Checkbox id="state-invalid" aria-invalid required />
        <FieldContent>
          <FieldLabel htmlFor="state-invalid">
            Akceptuję warunki przetwarzania danych
          </FieldLabel>
          <FieldError>Aby kontynuować, musisz zaakceptować warunki.</FieldError>
        </FieldContent>
      </Field>
    </FieldGroup>
  )
}

Karty wyboru

Owiń cały Field w FieldLabel, żeby zamienić go w kartę do zaznaczenia: cała karta przełącza pole wyboru, a zaznaczona karta nabiera odcienia marki. Karty stosuj dla opcji, które potrzebują ceny, obrazu lub dłuższego opisu.

import { Checkbox } from "@/components/ui/checkbox"
import {
  Field,
  FieldContent,
  FieldDescription,
  FieldLabel,
  FieldTitle,
} from "@/components/ui/field"

const addons = [
  {
    id: "backups",
    title: "Codzienne kopie zapasowe",
    description: "Przywracanie do dowolnego momentu z ostatnich 30 dni.",
    price: "+12 $/mies.",
    checked: true,
  },
  {
    id: "analytics",
    title: "Analityka witryny",
    description: "Raporty ruchu i konwersji z poszanowaniem prywatności.",
    price: "+$9/mo",
    checked: false,
  },
]

export default function CheckboxChoiceCard() {
  return (
    <div className="flex w-full max-w-md flex-col gap-3">
      {addons.map((addon) => (
        <FieldLabel key={addon.id} htmlFor={`addon-${addon.id}`}>
          <Field orientation="horizontal">
            <Checkbox id={`addon-${addon.id}`} defaultChecked={addon.checked} />
            <FieldContent>
              <FieldTitle>{addon.title}</FieldTitle>
              <FieldDescription>{addon.description}</FieldDescription>
            </FieldContent>
            <span className="text-sm font-medium text-foreground tabular-nums">
              {addon.price}
            </span>
          </Field>
        </FieldLabel>
      ))}
    </div>
  )
}

Grupa

Powiązane pola wyboru umieść w FieldSet z FieldLegend, żeby technologie wspomagające odczytały pytanie — Powiadom mnie e-mailem, gdy… — raz, przy wejściu do grupy, zamiast zostawiać każdą opcję bez kontekstu. Nadaj im ten sam name i różne wartości value, żeby wysłać je jako listę.

Wyślij mi e-mail, gdy
import { Checkbox } from "@/components/ui/checkbox"
import {
  Field,
  FieldDescription,
  FieldGroup,
  FieldLabel,
  FieldLegend,
  FieldSet,
} from "@/components/ui/field"

const events = [
  { id: "deploy-succeeded", label: "Wdrożenie się powiedzie", checked: false },
  { id: "deploy-failed", label: "Wdrożenie się nie powiedzie", checked: true },
  { id: "domain-expiring", label: "Domena wkrótce wygaśnie", checked: true },
  { id: "usage-limit", label: "Wykorzystanie osiągnie limit", checked: true },
]

export default function CheckboxFieldset() {
  return (
    <FieldSet className="w-full max-w-sm">
      <FieldLegend variant="label">Wyślij mi e-mail, gdy</FieldLegend>
      <FieldDescription>
        Na każde zdarzenie wysyłamy najwyżej jeden e-mail na godzinę.
      </FieldDescription>
      <FieldGroup data-slot="checkbox-group" className="gap-3">
        {events.map((event) => (
          <Field key={event.id} orientation="horizontal">
            <Checkbox
              id={event.id}
              name="events"
              value={event.id}
              defaultChecked={event.checked}
            />
            <FieldLabel htmlFor={event.id} className="font-normal">
              {event.label}
            </FieldLabel>
          </Field>
        ))}
      </FieldGroup>
    </FieldSet>
  )
}

Wytyczne

Kiedy używać

  • Do pojedynczej, niezależnej zgody: Zapamiętaj mnie, Akceptuję regulamin.
  • Do wyboru dowolnej liczby opcji z listy, także żadnej.
  • Gdy zmiana zaczyna obowiązywać dopiero po wysłaniu formularza.

Kiedy nie używać

  • Do ustawień, które działają od razu — użyj komponentu Switch.
  • Do wyboru dokładnie jednej opcji z zestawu — użyj komponentu Radio Group.
  • Gdy opcji jest więcej niż około dziesięciu — użyj komponentu Combobox lub Select z wyborem wielokrotnym.

Etykiety

Formułuj etykiety twierdząco, tak by opisywały stan zaznaczony. Nikt nie powinien rozszyfrowywać podwójnego przeczenia, żeby wiedzieć, co zrobi zaznaczenie pola.

Wysyłaj mi aktualności o produkcie

DobrzeStan zaznaczony czyta się jak wyraźne „tak”.

Nie przestawaj wysyłać mi aktualności

ŹleEtykiety z przeczeniem zamieniają stan niezaznaczony w podwójne przeczenie.

Układ

Układaj pola wyboru pionowo, po jednym w wierszu, z polem po lewej stronie etykiety. Pionowe listy szybciej się przegląda, a wszystkie etykiety są w nich wyrównane; poziome rzędy sprawdzają się tylko przy dwóch, trzech bardzo krótkich opcjach.

Dostępność

Widoczna kontrolka ma role="checkbox" i aria-checked ("mixed" w stanie pośrednim). Base UI renderuje ukryte natywne pole input na potrzeby wysyłania i walidacji formularza, przypisuje mu id pola wyboru i łączy sąsiedni <label htmlFor> z widoczną kontrolką przez aria-labelledby — dzięki temu etykiety są zarówno klikalne, jak i odczytywane.

KlawiszDziałanie
Tab
Przenosi fokus na pole wyboru. Fokus widać jako pierścień 2 px odsunięty od pola.
Spacja
Przełącza pole wyboru.
  • Zawsze dodawaj etykietę. Użyj FieldLabel z htmlFor, owiń pole wyboru etykietą albo użyj aria-label — ale tylko wtedy, gdy widoczna etykieta byłaby naprawdę zbędna (np. pole zaznaczania wiersza w tabeli z czytelnym nagłówkiem kolumny).
  • Krawędzie. Obramowanie niezaznaczonego pola używa tokenu input — 3:1 względem strony, zgodnie z WCAG 1.4.11. Stan zaznaczenia przekazuje też znacznik, a nie sam kolor.
  • Rozmiar celu. Pole ma 16 px, ale niewidoczny obszar kliknięcia powiększa je do 24 × 24 px (WCAG 2.2, 2.5.8), nie wpływając na układ. Pole przełącza też kliknięcie etykiety.
  • Grupy. Użyj FieldSet i FieldLegend, żeby pytanie grupy zostało odczytane. W hierarchii rodzic–dzieci aria-checked="mixed" na polu nadrzędnym informuje użytkowników czytników ekranu, że zaznaczono tylko część pól podrzędnych.

Dokumentacja API

Checkbox

Renderuje <span role="checkbox"> i ukryty <input>. Przyjmuje wszystkie propsy Checkbox.Root z Base UI.

PropTypDomyślnie
checked

Czy pole jest zaznaczone. W trybie kontrolowanym używaj razem z onCheckedChange.

booleanBrak wartości domyślnej
defaultChecked

Początkowy stan zaznaczenia w trybie niekontrolowanym.

booleanfalse
onCheckedChange

Wywoływana przy przełączeniu pola.

(checked: boolean, details) => voidBrak wartości domyślnej
indeterminate

Pokazuje kreskę i ustawia aria-checked="mixed" — dla pól nadrzędnych częściowo zaznaczonych grup.

booleanfalse
name

Sprawia, że pole jest wysyłane z formularzem.

stringBrak wartości domyślnej
value

Wartość wysyłana, gdy pole jest zaznaczone.

stringBrak wartości domyślnej
uncheckedValue

Wartość wysyłana, gdy pole nie jest zaznaczone. Domyślnie nic nie jest wysyłane.

stringBrak wartości domyślnej
required

Wymaga zaznaczenia pola przed wysłaniem formularza.

booleanfalse
disabled

Ignoruje interakcję i przygasza kontrolkę.

booleanfalse
readOnly

Pokazuje stan, ale nie pozwala go zmienić.

booleanfalse
id

Trafia do ukrytego inputa, żeby mógł się do niego odwołać <label htmlFor>.

stringBrak wartości domyślnej
aria-invalid

Pokazuje stan błędu. Łącz z data-invalid na Field.

booleanBrak wartości domyślnej