Button
Wywołuje akcję. Najważniejszy element interaktywny każdego interfejsu — i ten, którego najłatwiej nadużyć.
import { ArrowRightIcon } from "lucide-react"
import { Button } from "@/components/ui/button"
export default function ButtonDemo() {
return (
<div className="flex flex-wrap items-center gap-3">
<Button variant="outline">Anuluj</Button>
<Button>
Dalej
<ArrowRightIcon data-icon="inline-end" />
</Button>
</div>
)
}Anatomia
- 1KontenerMa wypełnienie, obramowanie i pierścień fokusu zależne od wariantu. Jego wysokości pokrywają się z wysokościami pól tekstowych.
- 2Ikona na początkuOpcjonalna. Rozmiar i odstęp nadaje jej przycisk; po tej stronie padding się zmniejsza.
- 3EtykietaCzasownik, który nazywa efekt; wielka litera tylko na początku.
- 4Ikona na końcuOpcjonalna. Sygnalizuje kierunek albo menu.
Instalacja
$ pnpm dlx shadcn@latest add https://www.prfct.dev/r/button.jsonUżycie
import { Button } from "@/components/ui/button"<Button variant="outline">Cancel</Button>
<Button>Save changes</Button>Przykłady
Warianty
Siedem wariantów wyraża hierarchię, nie dekorację. Główny przycisk prfct jest celowo monochromatyczny: kolor zarezerwowano dla znaczenia — zaznaczenia, fokusu, statusu — więc jedyna kolorowa akcja na powierzchni zawsze jest tą najważniejszą.
import { Button } from "@/components/ui/button"
export default function ButtonVariants() {
return (
<div className="flex flex-wrap items-center justify-center gap-3">
<Button>Główny</Button>
<Button variant="brand">Marka</Button>
<Button variant="secondary">Drugorzędny</Button>
<Button variant="outline">Konturowy</Button>
<Button variant="ghost">Przezroczysty</Button>
<Button variant="destructive">Destrukcyjny</Button>
<Button variant="link">Link</Button>
</div>
)
}Rozmiary
Przyciski mają pięć wysokości: xs 28, sm 32, default 36, lg 40 i xl 48 pikseli. Pola tekstowe i listy wyboru mają rozmiary sm, default i lg o tych samych wysokościach, więc kontrolki w jednym rzędzie zawsze są wyrównane. Przyciski z samą ikoną są kwadratowe w każdym rozmiarze.
import { PlusIcon } from "lucide-react"
import { Button } from "@/components/ui/button"
export default function ButtonSizes() {
return (
<div className="flex flex-col items-center gap-6">
<div className="flex flex-wrap items-center justify-center gap-3">
<Button size="xs">Bardzo mały</Button>
<Button size="sm">Mały</Button>
<Button>Domyślny</Button>
<Button size="lg">Duży</Button>
<Button size="xl">Bardzo duży</Button>
</div>
<div className="flex flex-wrap items-center justify-center gap-3">
<Button size="icon-xs" variant="outline" aria-label="Dodaj element">
<PlusIcon />
</Button>
<Button size="icon-sm" variant="outline" aria-label="Dodaj element">
<PlusIcon />
</Button>
<Button size="icon" variant="outline" aria-label="Dodaj element">
<PlusIcon />
</Button>
<Button size="icon-lg" variant="outline" aria-label="Dodaj element">
<PlusIcon />
</Button>
</div>
</div>
)
}Z ikonami
Oznacz ikony atrybutem data-icon="inline-start" lub data-icon="inline-end". Przycisk zmniejszy padding po tej stronie, żeby zachować równowagę optyczną, i sam dobierze rozmiar ikony — nigdy nie dodawaj klas rozmiaru do ikon wewnątrz przycisku.
import { DownloadIcon, ExternalLinkIcon, SendIcon } from "lucide-react"
import { Button } from "@/components/ui/button"
export default function ButtonWithIcon() {
return (
<div className="flex flex-wrap items-center justify-center gap-3">
<Button>
<SendIcon data-icon="inline-start" />
Wyślij zaproszenie
</Button>
<Button variant="outline">
<DownloadIcon data-icon="inline-start" />
Eksportuj CSV
</Button>
<Button variant="ghost">
Otwórz w nowej karcie
<ExternalLinkIcon data-icon="inline-end" />
</Button>
</div>
)
}<Button>
<SendIcon data-icon="inline-start" />
Send invite
</Button>Sama ikona
Przycisk z samą ikoną nie ma widocznego tekstu, więc musi mieć aria-label. Dodaj do niego podpowiedź, która powtarza etykietę — z myślą o widzących użytkownikach myszy.
import { CopyIcon, PencilIcon, Trash2Icon } from "lucide-react"
import { Button } from "@/components/ui/button"
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@/components/ui/tooltip"
const actions = [
{ label: "Edytuj", icon: PencilIcon },
{ label: "Duplikuj", icon: CopyIcon },
{ label: "Usuń", icon: Trash2Icon },
]
export default function ButtonIconOnly() {
return (
<div className="flex items-center gap-1">
{actions.map(({ label, icon: Icon }) => (
<Tooltip key={label}>
<TooltipTrigger
render={<Button variant="ghost" size="icon" aria-label={label} />}
>
<Icon />
</TooltipTrigger>
<TooltipContent>{label}</TooltipContent>
</Tooltip>
))}
</div>
)
}Ładowanie
loading zastępuje etykietę wskaźnikiem ładowania bez zmiany szerokości przycisku, ustawia aria-busy i blokuje kliknięcia — ale zostawia fokus klawiatury na przycisku, więc użytkownicy czytników ekranu i klawiatury nie tracą orientacji.
"use client"
import * as React from "react"
import { Button } from "@/components/ui/button"
export default function ButtonLoading() {
const [saving, setSaving] = React.useState(false)
function save() {
setSaving(true)
setTimeout(() => setSaving(false), 2000)
}
return (
<div className="flex flex-wrap items-center justify-center gap-3">
<Button loading={saving} onClick={save}>
Zapisz zmiany
</Button>
<Button variant="outline" loading>
Publikowanie
</Button>
</div>
)
}loading zachowuje jedno i drugie. Jeśli potrzebujesz innego układu, nadal możesz samodzielnie użyć komponentu Spinner.Jako link
Akcja, która przenosi w inne miejsce, jest linkiem, nawet jeśli wygląda jak przycisk: czytniki ekranu odczytują ją jako link, a użytkownicy mogą otworzyć ją w nowej karcie. Nadaj linkowi wygląd przycisku za pomocą buttonVariants, zamiast renderować Button jako link — Base UI nadałby mu role="button".
import { ArrowUpRightIcon } from "lucide-react"
import Link from "next/link"
import { buttonVariants } from "@/components/ui/button"
export default function ButtonAsLink() {
return (
<div className="flex flex-wrap items-center justify-center gap-3">
<Link href="/pl/docs/installation" className={buttonVariants()}>
Zacznij
</Link>
<a
href="https://base-ui.com"
target="_blank"
rel="noreferrer"
className={buttonVariants({ variant: "outline" })}
>
Dokumentacja Base UI
<ArrowUpRightIcon data-icon="inline-end" />
</a>
</div>
)
}import Link from "next/link"
import { buttonVariants } from "@/components/ui/button"
<Link href="/docs/installation" className={buttonVariants()}>
Get started
</Link>Wytyczne
Kiedy używać
- Do wywołania akcji na bieżącej stronie: wysłania formularza, otwarcia okna dialogowego, zapisu, usunięcia.
- Do rozpoczęcia procesu: Utwórz projekt, Zaproś członka zespołu.
Kiedy nie używać
- Do nawigacji. Użyj linku — ostylowanego za pomocą
buttonVariants, jeśli ma wyglądać jak przycisk. - Do włączania i wyłączania ustawienia — użyj komponentu Switch.
- Do wyboru jednej opcji z niewielkiego zestawu — użyj komponentu Toggle Group.
Hierarchia
Daj każdemu widokowi jedną akcję główną. Wszystkie pozostałe schodzą niżej — do outline, secondary lub ghost. Akcję główną umieść na końcu rzędu (w stopkach — wyrównaną do prawej), a akcje rezygnacji, takie jak Anuluj, na przeciwległym końcu.
import { Button } from "@/components/ui/button"
export default function ButtonHierarchy() {
return (
<div className="flex w-full max-w-md items-center justify-between gap-3 rounded-xl border bg-card p-4 shadow-raised">
<Button variant="ghost">Odrzuć</Button>
<div className="flex items-center gap-2">
<Button variant="outline">Zapisz szkic</Button>
<Button>Opublikuj</Button>
</div>
</div>
)
}Etykiety
Zacznij od czasownika i nazwij efekt: Zapisz zmiany, Usuń projekt, Wyślij 3 zaproszenia. Wielką literą pisz tylko pierwsze słowo, zmieść się w jednym–trzech słowach i nigdy nie kończ etykiety znakiem interpunkcyjnym. Etykieta musi mieć sens także wtedy, gdy czytnik ekranu odczyta ją bez kontekstu.
Dostępność
Przycisk prfct renderuje przez Base UI natywny <button>, więc bez dodatkowej pracy przyjmuje fokus, przekazuje czytnikom ekranu swoją rolę i aktywuje się klawiszami Enter i Spacja.
| Klawisz | Działanie |
|---|---|
Tab | Przenosi fokus na przycisk. Pierścień fokusu to obrys o grubości 2 px, odsunięty od krawędzi, widoczny w każdym motywie i w trybie wymuszonych kolorów. |
EnterSpacja | Aktywuje przycisk. |
- Rozmiar celu. Każdy rozmiar spełnia minimum 24 × 24 px z WCAG 2.2 (2.5.8): najmniejsze,
xsiicon-xs, mają 28 px. Tych dwóch używaj tylko w gęstych interfejsach obsługiwanych głównie kursorem, bo w cel o wielkości 28 px trudno trafić palcem. - Kontrast. Tekst etykiety na każdym wypełnionym wariancie osiąga 4,5:1 w obu trybach; zobacz Kolor.
- Stan wyłączony. Wyłączone przyciski wypadają z kolejności tabulacji. Jeśli użytkownik ma się dowiedzieć, dlaczego akcja jest niedostępna, zostaw przycisk włączony i wyjaśnij to po kliknięciu albo użyj
focusableWhenDisabledz podpowiedzią. - Ładowanie.
loadingustawiaaria-busy="true"i zachowuje fokus; połącz go z regionem na żywo w trybie polite albo z powiadomieniem, które przekaże wynik.
Dokumentacja API
Button
Renderuje element <button>. Przyjmuje wszystkie propsy komponentu Button z Base UI. Typ propsów jest eksportowany jako ButtonProps — przyda się w komponentach, które opakowują przycisk.
buttonVariants
Generator klas jest eksportowany, żeby inne elementy — przede wszystkim linki — mogły wyglądać jak przyciski, nie będąc nimi. Ikony oznaczone data-icon mają rozmiar i odstępy dokładnie takie jak w Button.
import { buttonVariants } from "@/components/ui/button"
<Link href="/pricing" className={buttonVariants({ variant: "outline", size: "sm" })}>
Pricing
</Link>