پرش به محتوا

گروه دکمه

فوزها دکمه‌های شکافته، نوار ابزارها و افزونه‌های ورودی را در یک واحد قاب‌دار ادغام می‌کنند — یک حاشیهٔ ضخیم، یک سایهٔ سخت، یک فشار مشترک.

"use client"

import * as React from "react"

گروه دکمه، دکمه‌ها، ورودی‌ها و برچسب‌های مجاور را در یک کنترل segmented ادغام می‌کند. primitive زیرین ندارد — یک کانتینر ساده با role="group" قاب نئوبروتالیستی را نگه می‌دارد (یک حاشیهٔ ضخیم، یک سایهٔ سخت، یک انیمیشن فشردن مشترک)؛ فقط ButtonGroupSeparator به primitive می‌رسد و Separator را می‌پیچد.

برای این موارد از آن استفاده کنید:

  • دکمه‌های split — اقدام اصلی به‌همراه DropdownMenu از جایگزین‌ها: Save / Save as…، Merge / Squash.
  • Pager و نوار ابزار — Previous/Next، زوم داخل/خارج، سوئیچر نما در یک قاب.
  • افزونهٔ ورودی — فیلد جستجو با دکمهٔ submit، برچسب پیشوند URL، پسوند دکمهٔ کپی.

نصب

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

وابستگی‌های زیر را نصب کنید:

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

کد زیر را کپی کنید و در پروژهٔ خود بچسبانید.

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

مسیرهای import را متناسب با ساختار پروژهٔ خود به‌روزرسانی کنید.

استفاده

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

ترکیب

برای ساخت ButtonGroup از ترکیب زیر استفاده کنید:

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

دسترس‌پذیری

  • کامپوننت ButtonGroup سمت role را روی group تنظیم کرده است.
  • برای جابه‌جایی بین دکمه‌های گروه از Tab استفاده کنید.
  • برای برچسب‌گذاری گروه دکمه‌ها از aria-label یا aria-labelledby استفاده کنید.
<ButtonGroup aria-label="Button group">
  <Button>Button 1</Button>
  <Button>Button 2</Button>
</ButtonGroup>

ButtonGroup vs ToggleGroup

  • زمانی که می‌خواهید دکمه‌هایی را که یک کنش انجام می‌دهند گروه‌بندی کنید، از کامپوننت ButtonGroup استفاده کنید.
  • زمانی که می‌خواهید دکمه‌هایی را که یک حالت را جابه‌جا می‌کنند گروه‌بندی کنید، از کامپوننت ToggleGroup استفاده کنید.

نمونه‌ها

جهت‌گیری

پراپ orientation را تنظیم کنید تا چیدمان گروه دکمه‌ها تغییر کند.

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

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

اندازه

اندازهٔ دکمه‌ها را با پراپ size روی تک‌تک دکمه‌ها کنترل کنید.

import { PlusIcon } from "lucide-react"

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

تودرتو

کامپوننت‌های <ButtonGroup> را تودرتو قرار دهید تا گروه‌های دکمه با فاصله بسازید.

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

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

جداکننده

کامپوننت ButtonGroupSeparator دکمه‌ها را در یک گروه به‌صورت بصری جدا می‌کند.

دکمه‌های با نوع outline به جداکننده نیاز ندارند، زیرا از پیش حاشیه دارند. برای سایر انواع، استفاده از جداکننده برای بهبود سلسله‌مراتب بصری توصیه می‌شود.

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

تقسیم‌شده

یک گروه دکمهٔ تقسیم‌شده بسازید، با افزودن دو دکمه که با یک ButtonGroupSeparator از هم جدا شده‌اند.

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

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

Input

یک کامپوننت Input را با دکمه‌ها بپیچید.

import { SearchIcon } from "lucide-react"

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

Input Group

یک کامپوننت InputGroup را بپیچید تا چیدمان‌های ورودی پیچیده بسازید.

"use client"

import * as React from "react"

یک گروه دکمهٔ تقسیم‌شده با یک کامپوننت DropdownMenu بسازید.

"use client"

import {

Select

با یک کامپوننت Select جفت کنید.

"use client"

import * as React from "react"

Popover

با یک کامپوننت Popover استفاده کنید.

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

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

RTL

برای فعال‌سازی پشتیبانی RTL در Neobrutalism، راهنمای پیکربندی RTL را ببینید.

"use client"

import * as React from "react"

دسترسی‌پذیری

ButtonGroup مقدار role="group" را رندر می‌کند که به فناوری‌های کمکی می‌گوید دکمه‌ها با هم هستند، بدون تغییر رفتار صفحه‌کلید — هر دکمه tab stop خودش و فعال‌سازی بومی Enter / Space را نگه می‌دارد. گروه را با aria-label یا aria-labelledby نام‌گذاری کنید تا صفحه‌خوان زمینه را اعلام کند:

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

ButtonGroupSeparator جداکنندهٔ Base UI را می‌پیچد که role="separator" با aria-orientation متناظر رندر می‌کند — به‌عنوان divider اعلام می‌شود و هرگز focusable نیست. اگر یک tab stop واحد با ناوبری کلید جهت بین دکمه‌ها می‌خواهید، آن الگوی WAI-ARIA Toolbar است — این کامپوننت عمداً ترتیب tab ساده را نگه می‌دارد.

مرجع API

ButtonGroup

کامپوننت ButtonGroup کانتینری است که دکمه‌های مرتبط را با استایلی یکنواخت کنار هم گروه‌بندی می‌کند.

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

چند گروه دکمه را تودرتو قرار دهید تا چیدمان‌های پیچیده با فاصله بسازید. برای جزئیات بیشتر نمونهٔ تودرتو را ببینید.

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

ButtonGroupSeparator

کامپوننت ButtonGroupSeparator دکمه‌ها را در یک گروه به‌صورت بصری جدا می‌کند.

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

ButtonGroupText

از این کامپوننت برای نمایش متن در یک گروه دکمه استفاده کنید.

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

از پراپ asChild برای رندر یک کامپوننت سفارشی به‌عنوان متن استفاده کنید، برای مثال یک 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>
  )
}