דילוג לתוכן

קבוצת כפתורים

Fuses מאחד כפתורים מפוצלים, סרגלי כלים ותוספים להזנת נתונים ליחידה אחת ממוסגרת — מסגרת עבה אחת, צל קשיח אחד, לחיצה משותפת אחת.

"use client"

import * as React from "react"

ה-button group מאחד כפתורים, שדות ולייבלים סמוכים לבקרה מקטעת אחת. אין פרימיטיב מתחת — מיכל role="group" פשוט מחזיק את המסגרת הנאו-ברוטליסטית (מסגרת עבה אחת, צל חד אחד, אנימציית לחיצה משותפת); רק ButtonGroupSeparator מגיע לפרימיטיב, ועוטף Separator.

השתמשו בו כש:

  • כפתורים מפוצלים — פעולה ראשית ועוד DropdownMenu של חלופות: שמירה / שמירה בשם…, Merge / Squash.
  • דפדוף וסרגלי כלים — הקודם/הבא, זום פנימה/החוצה, מחליפי תצוגה באריזה אחת.
  • תוספות לשדות — שדה חיפוש עם כפתור שליחה, תווית קידומת 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", שאומר לטכנולוגיה מסייעת שהכפתורים שייכים יחד בלי לשנות התנהגות מקלדת — כל כפתור שומר על tab stop משלו והפעלה מקורית של Enter / Space. תנו שם לקבוצה עם aria-label או aria-labelledby כדי שקוראי מסך יכריזו על ההקשר:

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

ButtonGroupSeparator עוטף את Separator של Base UI, שמרנדר role="separator" עם aria-orientation מתאים — מוכרז כמפריד, אף פעם לא ניתן לפוקוס. אם אתם צריכים tab stop יחיד עם ניווט במקשי חצים בין כפתורים, זה דפוס Toolbar של WAI-ARIA — הרכיב הזה משאיר במכוון סדר 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 כדי להציג רכיב מותאם אישית בתור הטקסט, לדוגמה תווית.

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