Lewati ke konten

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 DropdownMenu berisi 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.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

Pasang dependensi berikut:

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

Salin dan tempel kode berikut ke proyek Anda.

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

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
└── ButtonGroupText

ButtonGroup vs ToggleGroup

  • Gunakan komponen ButtonGroup saat Anda ingin mengelompokkan tombol yang menjalankan suatu aksi.
  • Gunakan komponen ToggleGroup saat 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"

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.

PropTypeDefault
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.

PropTypeDefault
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.

PropTypeDefault
asChildbooleanfalse
<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>
  )
}