Grup tombol
Fuses menggabungkan tombol-tombol terpisah, bilah alat, dan ekstensi input menjadi satu unit berbingkai — satu garis tepi tebal, satu bayangan tajam, dan satu tombol tekan bersama.
"use client"
import * as React from "react"Button group melebur tombol, input, dan label yang berdampingan menjadi satu kontrol tersegmentasi. Tidak ada primitif di baliknya — kontainer role="group" polos yang memegang bingkai neobrutalisnya (satu garis tepi tebal, satu bayangan keras, satu animasi tekan bersama); hanya ButtonGroupSeparator yang memakai primitif, dengan membungkus Separator.
Gunakan saat:
- Split button — aksi utama plus
DropdownMenuberisi alternatifnya: Save / Save as…, Merge / Squash. - Pager dan toolbar — Previous/Next, perbesar/perkecil, pengalih tampilan yang dikemas dalam satu bingkai.
- Add-on input — kolom pencarian dengan tombol kirimnya, label prefiks URL, tombol salin sebagai sufiks.
Instalasi#
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
Pasang dependensi berikut:
pnpm add @base-ui/reactnpm install @base-ui/reactyarn add @base-ui/reactbun add @base-ui/react
Salin dan tempel kode berikut ke proyek Anda.
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,
}
Perbarui jalur import agar sesuai dengan struktur proyek Anda.
Penggunaan#
import {
ButtonGroup,
ButtonGroupSeparator,
ButtonGroupText,
} from "@/components/ui/button-group"<ButtonGroup>
<Button>Button 1</Button>
<Button>Button 2</Button>
</ButtonGroup>Komposisi#
Gunakan komposisi berikut untuk membangun ButtonGroup:
ButtonGroup
├── Button or Input
├── ButtonGroupSeparator
└── ButtonGroupTextButtonGroup vs ToggleGroup#
- Gunakan komponen
ButtonGroupsaat Anda ingin mengelompokkan tombol yang menjalankan suatu aksi. - Gunakan komponen
ToggleGroupsaat Anda ingin mengelompokkan tombol yang mengalihkan suatu keadaan.
Contoh#
Orientasi#
Setel prop orientation untuk mengubah tata letak grup tombol.
import { MinusIcon, PlusIcon } from "lucide-react"
import { Button } from "@/components/ui/button"Ukuran#
Kendalikan ukuran tombol menggunakan prop size pada masing-masing tombol.
import { PlusIcon } from "lucide-react"
import { Button } from "@/components/ui/button"Bersarang#
Sarangkan komponen <ButtonGroup> untuk membuat grup tombol dengan jarak antar elemen.
import { AudioLinesIcon, PlusIcon } from "lucide-react"
import { Button } from "@/components/ui/button"Pemisah#
Komponen ButtonGroupSeparator memisahkan tombol secara visual di dalam sebuah grup.
Tombol dengan varian outline tidak memerlukan pemisah karena sudah memiliki border. Untuk varian lain, pemisah disarankan untuk meningkatkan hierarki visual.
import { Button } from "@/components/ui/button"
import {
ButtonGroup,Terbagi#
Buat grup tombol terbagi dengan menambahkan dua tombol yang dipisahkan oleh ButtonGroupSeparator.
import { IconPlus } from "@tabler/icons-react"
import { Button } from "@/components/ui/button"Input#
Bungkus komponen Input dengan tombol.
import { SearchIcon } from "lucide-react"
import { Button } from "@/components/ui/button"Input Group#
Bungkus komponen InputGroup untuk membuat tata letak input yang kompleks.
"use client"
import * as React from "react"Dropdown Menu#
Buat grup tombol terbagi dengan komponen DropdownMenu.
"use client"
import {Select#
Pasangkan dengan komponen Select.
"use client"
import * as React from "react"Popover#
Gunakan dengan komponen Popover.
import { BotIcon, ChevronDownIcon } from "lucide-react"
import { Button } from "@/components/ui/button"RTL#
Untuk mengaktifkan dukungan RTL di Neobrutalism, lihat panduan konfigurasi RTL.
"use client"
import * as React from "react"Aksesibilitas#
ButtonGroup merender role="group", yang memberi tahu teknologi asistif bahwa tombol-tombol itu satu kesatuan tanpa mengubah perilaku keyboard — setiap tombol tetap punya tab stop sendiri dan aktivasi Enter / Space natif. Beri nama grup dengan aria-label atau aria-labelledby agar pembaca layar mengumumkan konteksnya:
<ButtonGroup aria-label="Pagination">
<Button>Previous</Button>
<Button>Next</Button>
</ButtonGroup>ButtonGroupSeparator membungkus Separator dari Base UI, yang dirender sebagai role="separator" dengan aria-orientation yang sesuai — diumumkan sebagai pemisah dan tidak pernah bisa difokus. Jika Anda butuh satu tab stop dengan navigasi tombol panah antar tombol, itu adalah pola Toolbar WAI-ARIA — komponen ini sengaja mempertahankan urutan tab biasa.
Referensi API#
ButtonGroup#
Komponen ButtonGroup adalah kontainer yang mengelompokkan tombol-tombol terkait dengan gaya yang konsisten.
| Prop | Type | Default |
|---|---|---|
orientation | "horizontal" | "vertical" | "horizontal" |
<ButtonGroup>
<Button>Button 1</Button>
<Button>Button 2</Button>
</ButtonGroup>Sarangkan beberapa grup tombol untuk membuat tata letak kompleks dengan jarak antar elemen. Lihat contoh bersarang untuk detail lebih lanjut.
<ButtonGroup>
<ButtonGroup />
<ButtonGroup />
</ButtonGroup>ButtonGroupSeparator#
Komponen ButtonGroupSeparator memisahkan tombol secara visual di dalam sebuah grup.
| Prop | Type | Default |
|---|---|---|
orientation | "horizontal" | "vertical" | "vertical" |
<ButtonGroup>
<Button>Button 1</Button>
<ButtonGroupSeparator />
<Button>Button 2</Button>
</ButtonGroup>ButtonGroupText#
Gunakan komponen ini untuk menampilkan teks di dalam grup tombol.
| Prop | Type | Default |
|---|---|---|
asChild | boolean | false |
<ButtonGroup>
<ButtonGroupText>Text</ButtonGroupText>
<Button>Button</Button>
</ButtonGroup>Gunakan prop asChild untuk merender komponen kustom sebagai teks, misalnya sebuah 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>
)
}