Przejdź do treści

Button Group

Łączy powiązane przyciski, pola i tekst w jedną kontrolkę, dzięki czemu odbiera się je jako jeden zestaw akcji.

Kod źródłowyRejestr
import { ArchiveIcon, ClockIcon, FlagIcon } from "lucide-react"

import { Button } from "@/components/ui/button"
import { ButtonGroup } from "@/components/ui/button-group"

export default function ButtonGroupDemo() {
  return (
    <ButtonGroup aria-label="Akcje wiadomości">
      <Button variant="outline">
        <ArchiveIcon data-icon="inline-start" />
        Archiwizuj
      </Button>
      <Button variant="outline">
        <ClockIcon data-icon="inline-start" />
        Odłóż
      </Button>
      <Button variant="outline">
        <FlagIcon data-icon="inline-start" />
        Oznacz flagą
      </Button>
    </ButtonGroup>
  )
}

Anatomia

  1. 1GrupaŁączy swoje elementy w jedną kontrolkę: obramowania się scalają, a wewnętrzne narożniki tracą zaokrąglenie. Ma role="group" i powinna mieć aria-label.
  2. 2AkcjaDowolny przycisk prfct. Trzymaj się jednego wariantu w całej grupie, żeby odbierano ją jako całość.
  3. 3SeparatorOddziela przyciski wypełnione — nie mają one własnych obramowań, które mogłyby się zetknąć.
  4. 4Przycisk menuOtwiera powiązane opcje — to wzorzec przycisku dzielonego. Ma tylko ikonę, więc potrzebuje aria-label.

Instalacja

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

Użycie

import { Button } from "@/components/ui/button"
import { ButtonGroup } from "@/components/ui/button-group"
<ButtonGroup aria-label="Message actions">
  <Button variant="outline">Archive</Button>
  <Button variant="outline">Snooze</Button>
  <Button variant="outline">Flag</Button>
</ButtonGroup>

Grupa usuwa zaokrąglenie wewnętrznych narożników swoich elementów i scala ich wspólne obramowania. Działa ze wszystkim, co ma atrybut data-slot — z przyciskami, polami tekstowymi, wyzwalaczami list wyboru i menu rozwijanych oraz z ButtonGroupText.

Przykłady

Orientacja

orientation="vertical" układa elementy jeden pod drugim i łączy je w pionie — to klasyczna kontrolka powiększenia.

import { MinusIcon, PlusIcon, ScanIcon } from "lucide-react"

import { Button } from "@/components/ui/button"
import { ButtonGroup } from "@/components/ui/button-group"

export default function ButtonGroupOrientation() {
  return (
    <ButtonGroup orientation="vertical" aria-label="Powiększenie mapy">
      <Button variant="outline" size="icon" aria-label="Powiększ">
        <PlusIcon />
      </Button>
      <Button variant="outline" size="icon" aria-label="Pomniejsz">
        <MinusIcon />
      </Button>
      <Button variant="outline" size="icon" aria-label="Dopasuj do ekranu">
        <ScanIcon />
      </Button>
    </ButtonGroup>
  )
}

Z separatorami

Warianty wypełnione nie mają obramowań, które mogłyby się połączyć, więc dodaj między elementami ButtonGroupSeparator, żeby dało się je od siebie odróżnić.

import { ClipboardPasteIcon, CopyIcon, ScissorsIcon } from "lucide-react"

import { Button } from "@/components/ui/button"
import { ButtonGroup, ButtonGroupSeparator } from "@/components/ui/button-group"

export default function ButtonGroupWithSeparator() {
  return (
    <ButtonGroup aria-label="Schowek">
      <Button variant="secondary" size="sm">
        <ScissorsIcon data-icon="inline-start" />
        Wytnij
      </Button>
      <ButtonGroupSeparator />
      <Button variant="secondary" size="sm">
        <CopyIcon data-icon="inline-start" />
        Kopiuj
      </Button>
      <ButtonGroupSeparator />
      <Button variant="secondary" size="sm">
        <ClipboardPasteIcon data-icon="inline-start" />
        Wklej
      </Button>
    </ButtonGroup>
  )
}

Z polami

Dołącz przycisk do pola wyszukiwania albo pola z wartością do skopiowania, a dla stałych prefiksów i sufiksów, takich jak protokoły i domeny, użyj ButtonGroupText.

https://
.prfct.dev
import { CopyIcon, SearchIcon } from "lucide-react"

import { Button } from "@/components/ui/button"
import { ButtonGroup, ButtonGroupText } from "@/components/ui/button-group"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"

export default function ButtonGroupWithInput() {
  return (
    <div className="flex w-full max-w-sm flex-col gap-6">
      <ButtonGroup className="w-full">
        <Input placeholder="Szukaj zgłoszeń" aria-label="Szukaj zgłoszeń" />
        <Button variant="outline" size="icon" aria-label="Szukaj">
          <SearchIcon />
        </Button>
      </ButtonGroup>
      <div className="flex flex-col gap-2">
        <Label htmlFor="subdomain">Adres URL przestrzeni roboczej</Label>
        <ButtonGroup className="w-full">
          <ButtonGroupText>https://</ButtonGroupText>
          <Input id="subdomain" defaultValue="acme" />
          <ButtonGroupText>.prfct.dev</ButtonGroupText>
          <Button variant="outline" size="icon" aria-label="Kopiuj adres URL">
            <CopyIcon />
          </Button>
        </ButtonGroup>
      </div>
    </div>
  )
}

Przycisk dzielony

Połącz akcję główną z menu alternatyw. Główny przycisk uruchamia najczęściej wybieraną opcję, a strzałka otwiera pozostałe.

import {
  ChevronDownIcon,
  GitMergeIcon,
  GitPullRequestIcon,
  LayersIcon,
} from "lucide-react"

import { Button } from "@/components/ui/button"
import { ButtonGroup, ButtonGroupSeparator } from "@/components/ui/button-group"
import {
  DropdownMenu,
  DropdownMenuContent,
  DropdownMenuGroup,
  DropdownMenuItem,
  DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"

export default function ButtonGroupSplitButton() {
  return (
    <ButtonGroup>
      <Button>
        <GitMergeIcon data-icon="inline-start" />
        Scal pull request
      </Button>
      <ButtonGroupSeparator className="bg-primary-foreground/30" />
      <DropdownMenu>
        <DropdownMenuTrigger
          render={<Button size="icon" aria-label="Więcej opcji scalania" />}
        >
          <ChevronDownIcon />
        </DropdownMenuTrigger>
        <DropdownMenuContent align="end" className="w-60">
          <DropdownMenuGroup>
            <DropdownMenuItem>
              <GitMergeIcon />
              Utwórz commit scalający
            </DropdownMenuItem>
            <DropdownMenuItem>
              <LayersIcon />
              Spłaszcz i scal
            </DropdownMenuItem>
            <DropdownMenuItem>
              <GitPullRequestIcon />
              Zmień bazę i scal
            </DropdownMenuItem>
          </DropdownMenuGroup>
        </DropdownMenuContent>
      </DropdownMenu>
    </ButtonGroup>
  )
}
<ButtonGroup>
  <Button>Merge pull request</Button>
  <ButtonGroupSeparator className="bg-primary-foreground/30" />
  <DropdownMenu>
    <DropdownMenuTrigger render={<Button size="icon" aria-label="More merge options" />}>
      <ChevronDownIcon />
    </DropdownMenuTrigger>
    <DropdownMenuContent align="end">…</DropdownMenuContent>
  </DropdownMenu>
</ButtonGroup>

Zagnieżdżone grupy

Grupy zagnieżdżone w grupie zachowują własne połączone krawędzie i są od siebie odsunięte — tak paski narzędzi zbierają powiązane narzędzia w zestawy.

import {
  AlignCenterIcon,
  AlignLeftIcon,
  AlignRightIcon,
  BoldIcon,
  ItalicIcon,
  UnderlineIcon,
} from "lucide-react"

import { Button } from "@/components/ui/button"
import { ButtonGroup } from "@/components/ui/button-group"

export default function ButtonGroupNested() {
  return (
    <ButtonGroup aria-label="Formatowanie tekstu">
      <ButtonGroup aria-label="Styl">
        <Button variant="outline" size="icon-sm" aria-label="Pogrubienie">
          <BoldIcon />
        </Button>
        <Button variant="outline" size="icon-sm" aria-label="Kursywa">
          <ItalicIcon />
        </Button>
        <Button variant="outline" size="icon-sm" aria-label="Podkreślenie">
          <UnderlineIcon />
        </Button>
      </ButtonGroup>
      <ButtonGroup aria-label="Wyrównanie">
        <Button variant="outline" size="icon-sm" aria-label="Wyrównaj do lewej">
          <AlignLeftIcon />
        </Button>
        <Button variant="outline" size="icon-sm" aria-label="Wyśrodkuj">
          <AlignCenterIcon />
        </Button>
        <Button variant="outline" size="icon-sm" aria-label="Wyrównaj do prawej">
          <AlignRightIcon />
        </Button>
      </ButtonGroup>
    </ButtonGroup>
  )
}

Wytyczne

Kiedy używać

  • Do niewielkiego zestawu powiązanych, równorzędnych akcji na tym samym obiekcie: Archiwizuj, Odłóż, Oznacz flagą.
  • Aby dołączyć akcję do pola, np. wyszukiwanie lub kopiowanie.
  • Do przycisków dzielonych, w których jedna akcja jest domyślna, a pozostałe są jej odmianami.

Kiedy nie używać

  • Do wyboru spośród opcji — użyj komponentu Toggle Group, który śledzi, co jest zaznaczone.
  • Do akcji o różnej wadze. Grupa sugeruje równorzędność; akcja główna i przycisk anulowania powinny być osobnymi przyciskami z odstępem między nimi.
  • Do długich list akcji. Powyżej czterech–pięciu elementów przenieś resztę do menu Dropdown Menu.

Zachowaj spójne warianty

Wszystkie elementy grupy powinny mieć ten sam wariant i ten sam rozmiar. Różne warianty rozbijają wspólną sylwetkę, dzięki której grupę odbiera się jako jedną kontrolkę.

DobrzeJeden wariant, jeden rozmiar: grupa wygląda jak jedna kontrolka.
ŹlePomieszane warianty wyglądają jak pomyłka i ukrywają, która akcja jest ważna.

Dostępność

  • Grupa renderuje role="group". Nadaj jej aria-label (lub aria-labelledby), który nazywa zestaw — Akcje wiadomości, Powiększenie mapy — żeby czytnik ekranu ogłosił kontekst, gdy fokus wejdzie do grupy.
  • Każdy element zachowuje własną semantykę i własne miejsce w kolejności tabulacji. W odróżnieniu od Toggle Group nie ma tu wędrującego fokusu (roving focus): przyciski w grupie to niezależne akcje.
  • Przyciski z samą ikoną wewnątrz grupy nadal potrzebują aria-label.
  • Element z fokusem trafia na wierzch, więc sąsiednie obramowanie nigdy nie przycina pierścienia fokusu.
KlawiszDziałanie
TabShiftTab
Przenosi fokus między przyciskami, polami i wyzwalaczami w grupie.
EnterSpacja
Aktywuje przycisk, na którym jest fokus.

Dokumentacja API

ButtonGroup

Renderuje <div> z role="group" i data-slot="button-group".

PropTypDomyślnie
orientation

Kierunek, w którym elementy są łączone.

"horizontal" | "vertical""horizontal"
aria-label

Nazywa grupę na potrzeby technologii wspomagających.

stringBrak wartości domyślnej
className

Klasy układu, np. w-full, aby rozciągnąć grupę z polem.

stringBrak wartości domyślnej

ButtonGroupText

Nieinteraktywny segment na prefiksy, sufiksy i etykiety. Renderuje <div>; użyj render, aby podmienić element.

PropTypDomyślnie
render

Zastępuje renderowany element z zachowaniem stylów.

ReactElement | (props, state) => ReactElementBrak wartości domyślnej
className

Dodatkowe klasy.

stringBrak wartości domyślnej

ButtonGroupSeparator

Cienka linia między elementami dla wariantów bez obramowań. Zbudowany na komponencie Separator.

PropTypDomyślnie
orientation

W grupach pionowych użyj horizontal.

"horizontal" | "vertical""vertical"
className

Pozwala nadpisać kolor na przyciskach wypełnionych, np. bg-primary-foreground/30.

stringBrak wartości domyślnej

buttonGroupVariants

Generator klas, eksportowany dla własnych elementów, które mają łączyć się tak jak grupa.