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
DropdownMenumet 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.jsonnpx shadcn@latest add https://neobrutalism.com/r/base/button-group.jsonyarn dlx shadcn@latest add https://neobrutalism.com/r/base/button-group.jsonbunx --bun shadcn@latest add https://neobrutalism.com/r/base/button-group.json
Installeer de volgende afhankelijkheden:
pnpm add @base-ui/reactnpm install @base-ui/reactyarn add @base-ui/reactbun add @base-ui/react
Kopieer en plak de volgende code in je project.
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
└── ButtonGroupTextButtonGroup vs ToggleGroup#
- Gebruik de component
ButtonGroupwanneer je knoppen wilt groeperen die een actie uitvoeren. - Gebruik de component
ToggleGroupwanneer 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"Dropdown Menu#
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.
| Prop | Type | Default |
|---|---|---|
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.
| Prop | Type | Default |
|---|---|---|
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.
| Prop | Type | Default |
|---|---|---|
asChild | boolean | false |
<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>
)
}