الانتقال إلى المحتوى

مجموعة أزرار

تدمج «Fuses» الأزرار المنفصلة وأشرطة الأدوات وملحقات الإدخال في وحدة واحدة محاطة بإطار — حدود سميكة واحدة، وظل بارز واحد، وزر ضغط مشترك واحد.

"use client"

import * as React from "react"

تدمج مجموعة الأزرار (button group) الأزرار والمدخلات والتسميات المتجاورة في عنصر تحكم مقطّع واحد. لا عنصر أساسي تحتها — حاوية role="group" عادية تملك إطار النيوبروتالية (حد سميك واحد، ظل حاد واحد، حركة ضغط مشتركة)؛ وحده ButtonGroupSeparator يستعين بعنصر أساسي، مغلفًا Separator.

استخدمه عندما:

  • أزرار منقسمة — إجراء أساسي مع DropdownMenu لبدائل: Save / Save as…، Merge / Squash.
  • أشرطة الصفحات والأدوات — Previous/Next، تكبير/تصغير، مبدّلات العرض في إطار واحد.
  • إضافات المدخلات — حقل بحث مع زر إرسال، تسمية بادئة 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 {
  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

اضبط خاصية 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"، فيخبر التقنيات المساعدة أن الأزرار تنتمي معًا دون تغيير سلوك لوحة المفاتيح — يحتفظ كل زر بنقطة تبويب خاصة وتفعيل أصلي عبر Enter / Space. سمِّ المجموعة بـ aria-label أو aria-labelledby حتى تعلن قارئات الشاشة السياق:

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

ButtonGroupSeparator يغلّف Base UI Separator الذي يعرض role="separator" مع aria-orientation المطابق — يُعلن كفاصل، وغير قابل للتركيز. إن احتجت نقطة تبويب واحدة مع تنقل بالأسهم بين الأزرار، فذلك نمط WAI-ARIA Toolbar — هذا المكوّن يُبقي ترتيب التبويب العادي عمدًا.

مرجع 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>
  )
}