Zum Inhalt springen

Button-Gruppe

Fuses vereint Schaltflächen, Symbolleisten und Eingabe-Add-ons in einer gerahmten Einheit – ein dicker Rahmen, ein harter Schatten, eine gemeinsame Betätigung.

"use client"

import * as React from "react"

Die Button-Gruppe verschmilzt benachbarte Buttons, Eingabefelder und Labels zu einem segmentierten Steuerelement. Darunter liegt kein Primitive – ein schlichter role="group"-Container trägt den neobrutalistischen Rahmen (ein dicker Rand, ein harter Schatten, eine gemeinsame Drück-Animation); nur ButtonGroupSeparator greift auf ein Primitive zurück und umhüllt Separator.

Die richtige Wahl für:

  • Split-Buttons – eine Primäraktion plus ein DropdownMenu mit Alternativen: Speichern / Speichern unter …, Merge / Squash.
  • Pager und Toolbars – Zurück/Weiter, Zoom rein/raus, Ansichtsumschalter dicht gepackt in einem Rahmen.
  • Input-Add-ons – ein Suchfeld mit seinem Absende-Button, ein URL-Präfix-Label, ein Kopier-Button als Suffix.

Installation

pnpm dlx shadcn@latest add https://neobrutalism.com/r/base/button-group.json
npx shadcn@latest add https://neobrutalism.com/r/base/button-group.json
yarn dlx shadcn@latest add https://neobrutalism.com/r/base/button-group.json
bunx --bun shadcn@latest add https://neobrutalism.com/r/base/button-group.json

Installiere die folgenden Abhängigkeiten:

pnpm add @base-ui/react
npm install @base-ui/react
yarn add @base-ui/react
bun add @base-ui/react

Kopiere den folgenden Code und füge ihn in dein Projekt ein.

components/ui/button-group.tsx
import { cva, type VariantProps } from "class-variance-authority"
import { Slot } from "radix-ui"

import { cn } from "@/lib/utils"
import { Separator } from "@/components/ui/separator"

const buttonGroupVariants = cva(
  cn(
    // The GROUP itself is the cohesive NeoBrutalist unit: a single bold border +
    // one hard offset shadow wrapping every segment, so it reads as ONE unit.
    // The frame animates so the whole group can press like a single Button.
    "relative inline-flex w-fit items-stretch border-2 border-foreground bg-background shadow-md transition-all duration-200",
    // Segments become flush fills — the frame owns the border, shadow and corners.
    "[&>*]:rounded-none",
    "[&>:is(button,a)]:border-0 [&>:is(button,a)]:shadow-none!",
    // Segments never translate on their own — only the whole frame moves.
    "[&>:is(button,a)]:hover:translate-y-0! [&>:is(button,a)]:active:translate-x-0! [&>:is(button,a)]:active:translate-y-0!",
    // The WHOLE group presses into its shadow like a single Button: hover sinks it
    // toward the shadow, press sits it flush (shadow gone).
    "has-[:is(button,a):hover]:translate-y-1 has-[:is(button,a):hover]:shadow",
    "has-[:is(button,a):active]:translate-y-2 has-[:is(button,a):active]:translate-x-1 has-[:is(button,a):active]:shadow-none",
    // When a group CONTAINS nested groups it becomes a plain gapped container
    // (no frame / shadow / press) so each nested sub-group is its own pressable
    // unit instead of pressing the whole outer frame.
    "has-[>[data-slot=button-group]]:gap-2 has-[>[data-slot=button-group]]:border-0 has-[>[data-slot=button-group]]:bg-transparent has-[>[data-slot=button-group]]:shadow-none! has-[>[data-slot=button-group]]:translate-x-0! has-[>[data-slot=button-group]]:translate-y-0!",
    // The hovered/pressed segment darkens in place via a full-bleed inset tint, so
    // you can still tell which segment you're acting on while the frame presses.
    "[&>:is(button,a):hover]:shadow-[inset_0_0_0_999px_#00000012]! [&>:is(button,a):active]:shadow-[inset_0_0_0_999px_#0000001f]!",

    "dark:[&>:is(button,a):hover]:shadow-[inset_0_0_0_999px_#ffffff1f]! dark:[&>:is(button,a):active]:shadow-[inset_0_0_0_999px_#ffffff33]!",
    // Solid dark segments (secondary) always lighten on hover — a dark tint would
    // be invisible on a black button in light mode.
    "[&>:is(button,a)[data-variant=secondary]:hover]:shadow-[inset_0_0_0_999px_#ffffff2b]! [&>:is(button,a)[data-variant=secondary]:active]:shadow-[inset_0_0_0_999px_#ffffff45]!",
    "[&>:is(button,a):focus-visible]:relative [&>:is(button,a):focus-visible]:z-10"
  ),
  {
    variants: {
      orientation: {
        // A single crisp seam between segments (logical side, so RTL-correct).
        horizontal:
          "flex-row [&>*:not(:first-child)]:border-s-2! [&>*:not(:first-child)]:border-foreground",
        vertical:
          "flex-col [&>*:not(:first-child)]:border-t-2! [&>*:not(:first-child)]:border-foreground",
      },
    },
    defaultVariants: {
      orientation: "horizontal",
    },
  }
)

function ButtonGroup({
  className,
  orientation,
  ...props
}: React.ComponentProps<"div"> & VariantProps<typeof buttonGroupVariants>) {
  return (
    <div
      role="group"
      data-slot="button-group"
      data-orientation={orientation ?? "horizontal"}
      className={cn(buttonGroupVariants({ orientation }), className)}
      {...props}
    />
  )
}

function ButtonGroupText({
  className,
  asChild = false,
  ...props
}: React.ComponentProps<"div"> & {
  asChild?: boolean
}) {
  const Comp = asChild ? Slot.Root : "div"

  return (
    <Comp
      data-slot="button-group-text"
      className={cn(
        "inline-flex items-center gap-2 bg-muted px-3 font-head text-sm font-medium text-foreground [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4",
        className
      )}
      {...props}
    />
  )
}

function ButtonGroupSeparator({
  className,
  orientation = "vertical",
  ...props
}: React.ComponentProps<typeof Separator>) {
  return (
    <Separator
      data-slot="button-group-separator"
      orientation={orientation}
      className={cn(
        "relative z-10 self-stretch bg-border data-horizontal:h-0.5 data-horizontal:w-auto data-vertical:h-auto data-vertical:w-0.5",
        className
      )}
      {...props}
    />
  )
}

export {
  ButtonGroup,
  ButtonGroupSeparator,
  ButtonGroupText,
  buttonGroupVariants,
}

Passe die Import-Pfade an die Struktur deines Projekts an.

Verwendung

import {
  ButtonGroup,
  ButtonGroupSeparator,
  ButtonGroupText,
} from "@/components/ui/button-group"
<ButtonGroup>
  <Button>Button 1</Button>
  <Button>Button 2</Button>
</ButtonGroup>

Komposition

Verwende die folgende Komposition, um eine ButtonGroup aufzubauen:

ButtonGroup
├── Button or Input
├── ButtonGroupSeparator
└── ButtonGroupText

ButtonGroup vs ToggleGroup

  • Verwende die ButtonGroup-Komponente, wenn du Buttons gruppieren willst, die eine Aktion ausführen.
  • Verwende die ToggleGroup-Komponente, wenn du Buttons gruppieren willst, die einen Zustand umschalten.

Beispiele

Ausrichtung

Setze die Prop orientation, um das Layout der Button-Gruppe zu ändern.

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

import { Button } from "@/components/ui/button"

Größe

Steuere die Größe der Buttons mit der Prop size an einzelnen Buttons.

import { PlusIcon } from "lucide-react"

import { Button } from "@/components/ui/button"

Verschachtelt

Verschachtele <ButtonGroup>-Komponenten, um Button-Gruppen mit Abständen zu erstellen.

import { AudioLinesIcon, PlusIcon } from "lucide-react"

import { Button } from "@/components/ui/button"

Separator

Die ButtonGroupSeparator-Komponente trennt Buttons innerhalb einer Gruppe optisch.

Buttons mit der Variante outline benötigen keinen Separator, da sie bereits einen Rahmen haben. Für andere Varianten wird ein Separator empfohlen, um die visuelle Hierarchie zu verbessern.

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

Geteilt

Erstelle eine geteilte Button-Gruppe, indem du zwei Buttons hinzufügst, die durch einen ButtonGroupSeparator getrennt sind.

import { IconPlus } from "@tabler/icons-react"

import { Button } from "@/components/ui/button"

Input

Umschließe eine Input-Komponente mit Buttons.

import { SearchIcon } from "lucide-react"

import { Button } from "@/components/ui/button"

Input Group

Umschließe eine InputGroup-Komponente, um komplexe Eingabe-Layouts zu erstellen.

"use client"

import * as React from "react"

Erstelle eine geteilte Button-Gruppe mit einer DropdownMenu-Komponente.

"use client"

import {

Select

Kombiniere sie mit einer Select-Komponente.

"use client"

import * as React from "react"

Popover

Verwende sie mit einer Popover-Komponente.

import { BotIcon, ChevronDownIcon } from "lucide-react"

import { Button } from "@/components/ui/button"

RTL

Um die RTL-Unterstützung in Neobrutalism zu aktivieren, siehe die RTL-Konfigurationsanleitung.

"use client"

import * as React from "react"

Barrierefreiheit

ButtonGroup rendert role="group" und teilt assistiven Technologien damit mit, dass die Buttons zusammengehören, ohne das Tastaturverhalten zu ändern – jeder Button behält seinen eigenen Tab-Stopp und die native Aktivierung per Enter / Space. Benenne die Gruppe mit aria-label oder aria-labelledby, damit Screenreader den Kontext ankündigen:

<ButtonGroup aria-label="Pagination">
  <Button>Previous</Button>
  <Button>Next</Button>
</ButtonGroup>

ButtonGroupSeparator umhüllt den Base UI Separator, der role="separator" mit passender aria-orientation rendert – angekündigt als Trenner, nie fokussierbar. Brauchst du einen einzelnen Tab-Stopp mit Pfeiltasten-Navigation zwischen den Buttons, ist das das WAI-ARIA-Toolbar-Pattern – diese Komponente bleibt bewusst bei der gewöhnlichen Tab-Reihenfolge.

API-Referenz

ButtonGroup

Die ButtonGroup-Komponente ist ein Container, der verwandte Buttons mit einheitlichem Stil gruppiert.

PropTypeDefault
orientation"horizontal" | "vertical""horizontal"
<ButtonGroup>
  <Button>Button 1</Button>
  <Button>Button 2</Button>
</ButtonGroup>

Verschachtele mehrere Button-Gruppen, um komplexe Layouts mit Abständen zu erstellen. Weitere Details findest du im Beispiel verschachtelt.

<ButtonGroup>
  <ButtonGroup />
  <ButtonGroup />
</ButtonGroup>

ButtonGroupSeparator

Die ButtonGroupSeparator-Komponente trennt Buttons innerhalb einer Gruppe optisch.

PropTypeDefault
orientation"horizontal" | "vertical""vertical"
<ButtonGroup>
  <Button>Button 1</Button>
  <ButtonGroupSeparator />
  <Button>Button 2</Button>
</ButtonGroup>

ButtonGroupText

Verwende diese Komponente, um Text innerhalb einer Button-Gruppe anzuzeigen.

PropTypeDefault
asChildbooleanfalse
<ButtonGroup>
  <ButtonGroupText>Text</ButtonGroupText>
  <Button>Button</Button>
</ButtonGroup>

Verwende die Prop asChild, um eine benutzerdefinierte Komponente als Text zu rendern, zum Beispiel ein Label.

import { ButtonGroupText } from "@/components/ui/button-group"
import { Label } from "@/components/ui/label"
 
export function ButtonGroupTextDemo() {
  return (
    <ButtonGroup>
      <ButtonGroupText asChild>
        <Label htmlFor="name">Text</Label>
      </ButtonGroupText>
      <Input placeholder="Type something here..." id="name" />
    </ButtonGroup>
  )
}