Naar inhoud

Knoppengroep

Het combineert knoppen, werkbalken en invoer-add-ons tot één omkaderd geheel — één dikke rand, één scherpe schaduw, één gezamenlijke druk.

"use client"

import * as React from "react"

De button group smeedt naast elkaar liggende knoppen, invoervelden en labels samen tot één gesegmenteerde control. Er zit geen primitive onder — een kale container met role="group" draagt het neobrutalistische frame (één dikke rand, één harde schaduw, één gedeelde indrukanimatie); alleen ButtonGroupSeparator gebruikt een primitive en wikkelt Separator.

Gebruik deze component bij:

  • Split-buttons — een primaire actie plus een DropdownMenu met alternatieven: Opslaan / Opslaan als…, Merge / Squash.
  • Pagers en werkbalken — Vorige/Volgende, in- en uitzoomen, weergaveschakelaars strak samen in één frame.
  • Input-add-ons — een zoekveld met bijbehorende verzendknop, een label met URL-prefix, een kopieerknop als suffix.

Installatie

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

Installeer de volgende afhankelijkheden:

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

Kopieer en plak de volgende code in je project.

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,
}

Werk de import-paden bij zodat ze overeenkomen met je projectstructuur.

Gebruik

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

Compositie

Gebruik de volgende compositie om een ButtonGroup te bouwen:

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

ButtonGroup vs ToggleGroup

  • Gebruik de component ButtonGroup wanneer je knoppen wilt groeperen die een actie uitvoeren.
  • Gebruik de component ToggleGroup wanneer je knoppen wilt groeperen die een status omschakelen.

Voorbeelden

Oriëntatie

Stel de prop orientation in om de lay-out van de knopgroep te wijzigen.

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

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

Grootte

Bepaal de grootte van de knoppen met de prop size op afzonderlijke knoppen.

import { PlusIcon } from "lucide-react"

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

Genest

Nest <ButtonGroup>-componenten om knopgroepen met ruimte ertussen te maken.

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

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

Scheiding

De component ButtonGroupSeparator scheidt knoppen binnen een groep visueel.

Knoppen met de variant outline hebben geen scheiding nodig, omdat ze al een rand hebben. Voor andere varianten wordt een scheiding aanbevolen om de visuele hiërarchie te verbeteren.

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

Gesplitst

Maak een gesplitste knopgroep door twee knoppen toe te voegen die worden gescheiden door een ButtonGroupSeparator.

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

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

Input

Wikkel een Input-component in knoppen.

import { SearchIcon } from "lucide-react"

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

Input Group

Wikkel een InputGroup-component om complexe invoer-lay-outs te maken.

"use client"

import * as React from "react"

Maak een gesplitste knopgroep met een DropdownMenu-component.

"use client"

import {

Select

Combineer met een Select-component.

"use client"

import * as React from "react"

Popover

Gebruik met een Popover-component.

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

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

RTL

Om RTL-ondersteuning in Neobrutalism in te schakelen, zie de RTL-configuratiegids.

"use client"

import * as React from "react"

Toegankelijkheid

ButtonGroup rendert role="group", wat hulptechnologie vertelt dat de knoppen bij elkaar horen zonder het toetsenbordgedrag te veranderen — elke knop houdt zijn eigen tabstop en zijn native activatie met Enter / Space. Geef de groep een naam met aria-label of aria-labelledby, zodat schermlezers de context aankondigen:

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

ButtonGroupSeparator wikkelt de Base UI Separator, die role="separator" rendert met de bijbehorende aria-orientation — aangekondigd als scheiding en nooit focusbaar. Heb je één tabstop nodig met pijltjesnavigatie tussen de knoppen, dan zoek je het WAI-ARIA Toolbar-patroon — deze component houdt bewust de gewone tabvolgorde aan.

API-referentie

ButtonGroup

De component ButtonGroup is een container die gerelateerde knoppen met een consistente stijl groepeert.

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

Nest meerdere knopgroepen om complexe lay-outs met ruimte ertussen te maken. Zie het voorbeeld genest voor meer details.

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

ButtonGroupSeparator

De component ButtonGroupSeparator scheidt knoppen binnen een groep visueel.

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

ButtonGroupText

Gebruik deze component om tekst binnen een knopgroep weer te geven.

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

Gebruik de prop asChild om een aangepaste component als tekst te renderen, bijvoorbeeld een 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>
  )
}