# Button group

> Buttons joined into one control, with shared seams, separators, text addons, nesting and vertical stacks.

Docs: https://hextaui.com/docs/button-group
Markdown: https://hextaui.com/docs/button-group.md

```tsx title="components/examples/button-group/demo.tsx"
import {
  IconArchive,
  IconClock,
  IconDotsVertical,
  IconFlag,
} from "@tabler/icons-react"

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

export function ButtonGroupDemo() {
  return (
    <ButtonGroup aria-label="Message actions">
      <Button variant="outline">
        <IconArchive data-icon="inline-start" />
        <span className="max-sm:sr-only">Archive</span>
      </Button>
      <Button variant="outline">
        <IconFlag data-icon="inline-start" />
        <span className="max-sm:sr-only">Report</span>
      </Button>
      <Button variant="outline">
        <IconClock data-icon="inline-start" />
        <span className="max-sm:sr-only">Snooze</span>
      </Button>
      <Button variant="outline" size="icon" aria-label="More actions">
        <IconDotsVertical />
      </Button>
    </ButtonGroup>
  )
}
```

## Installation

### CLI

```bash
npx shadcn@latest add https://hextaui.com/r/button-group.json
```

This adds the component, the HextaUI theme tokens and any HextaUI components it depends on.

### Manual

Add the HextaUI theme tokens (https://hextaui.com/docs/installation#theme) to your global CSS if you haven't yet, then install the dependencies.

```bash
pnpm add @base-ui/react class-variance-authority cn
```

Copy and paste the following code into your project.

```tsx title="components/ui/button-group.tsx"
"use client"

import * as React from "react"
import { mergeProps } from "@base-ui/react/merge-props"
import { useRender } from "@base-ui/react/use-render"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "cn"

import { Separator, type SeparatorProps } from "@/components/ui/separator"

const buttonGroupVariants = cva(
  "group/button-group flex w-fit max-w-full min-w-0 items-stretch has-[>[data-slot=button-group]]:gap-2 [&>*:focus-within]:relative [&>*:focus-within]:z-10 [&>[aria-invalid=true]:not(:focus-within)]:relative [&>[aria-invalid=true]:not(:focus-within)]:z-1 [&>[data-slot=button]:active]:translate-none! [&>[data-slot=button]:active]:scale-none! [&>[data-slot=input]]:min-w-0 [&>[data-slot=input]]:flex-1 [&>[data-slot=select-trigger]:not([class*='w-'])]:w-fit [&>input]:min-w-0 [&>input]:flex-1",
  {
    variants: {
      orientation: {
        horizontal:
          "flex-row [&:not(:has(>[data-slot=button-group]))>:not([data-slot=button-status],[data-base-ui-focus-guard],span[aria-owns],input[aria-hidden=true],select[aria-hidden=true]):has(~:not([data-slot=button-status],[data-base-ui-focus-guard],span[aria-owns],input[aria-hidden=true],select[aria-hidden=true]))]:rounded-e-none [&:not(:has(>[data-slot=button-group]))>:not([data-slot=button-status],[data-base-ui-focus-guard],span[aria-owns],input[aria-hidden=true],select[aria-hidden=true])~:not([data-slot=button-status],[data-base-ui-focus-guard],span[aria-owns],input[aria-hidden=true],select[aria-hidden=true])]:-ms-(--hairline) [&:not(:has(>[data-slot=button-group]))>:not([data-slot=button-status],[data-base-ui-focus-guard],span[aria-owns],input[aria-hidden=true],select[aria-hidden=true])~:not([data-slot=button-status],[data-base-ui-focus-guard],span[aria-owns],input[aria-hidden=true],select[aria-hidden=true])]:rounded-s-none",
        vertical:
          "flex-col [&:not(:has(>[data-slot=button-group]))>:not([data-slot=button-status],[data-base-ui-focus-guard],span[aria-owns],input[aria-hidden=true],select[aria-hidden=true]):has(~:not([data-slot=button-status],[data-base-ui-focus-guard],span[aria-owns],input[aria-hidden=true],select[aria-hidden=true]))]:rounded-b-none [&:not(:has(>[data-slot=button-group]))>:not([data-slot=button-status],[data-base-ui-focus-guard],span[aria-owns],input[aria-hidden=true],select[aria-hidden=true])~:not([data-slot=button-status],[data-base-ui-focus-guard],span[aria-owns],input[aria-hidden=true],select[aria-hidden=true])]:-mt-(--hairline) [&:not(:has(>[data-slot=button-group]))>:not([data-slot=button-status],[data-base-ui-focus-guard],span[aria-owns],input[aria-hidden=true],select[aria-hidden=true])~:not([data-slot=button-status],[data-base-ui-focus-guard],span[aria-owns],input[aria-hidden=true],select[aria-hidden=true])]:rounded-t-none",
      },
    },
    defaultVariants: {
      orientation: "horizontal",
    },
  }
)

type ButtonGroupOrientation = NonNullable<
  VariantProps<typeof buttonGroupVariants>["orientation"]
>

type ButtonGroupProps = useRender.ComponentProps<"div"> & {
  orientation?: ButtonGroupOrientation
}

const ButtonGroupContext =
  React.createContext<ButtonGroupOrientation>("horizontal")

function dataAttributes(slot: string, extra?: Record<string, string>) {
  return { "data-slot": slot, ...extra } as Record<string, string>
}

function ButtonGroup({
  className,
  orientation = "horizontal",
  render,
  ...props
}: ButtonGroupProps) {
  const element = useRender({
    defaultTagName: "div",
    render,
    props: mergeProps<"div">(
      {
        role: "group",
        className: cn(buttonGroupVariants({ orientation }), className),
      },
      props,
      dataAttributes("button-group", { "data-orientation": orientation })
    ),
  })

  return (
    <ButtonGroupContext.Provider value={orientation}>
      {element}
    </ButtonGroupContext.Provider>
  )
}

function ButtonGroupText({
  className,
  render,
  ...props
}: useRender.ComponentProps<"div">) {
  return useRender({
    defaultTagName: "div",
    render,
    props: mergeProps<"div">(
      {
        className: cn(
          "flex max-w-full min-w-0 shrink-0 items-center gap-2 rounded-md bg-muted px-2.5 text-sm font-medium whitespace-nowrap inset-ring-(length:--hairline) inset-ring-border dark:inset-ring-input forced-colors:border [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 [&_svg:not([class*='text-'])]:text-muted-foreground",
          className
        ),
      },
      props,
      dataAttributes("button-group-text")
    ),
  })
}

type ButtonGroupSeparatorProps = Omit<SeparatorProps, "align" | "children">

function mergeClassName<State>(
  base: string,
  className: string | ((state: State) => string | undefined) | undefined
) {
  return typeof className === "function"
    ? (state: State) => cn(base, className(state))
    : cn(base, className)
}

function ButtonGroupSeparator({
  className,
  orientation,
  ...props
}: ButtonGroupSeparatorProps) {
  const groupOrientation = React.useContext(ButtonGroupContext)
  const resolvedOrientation =
    orientation ??
    (groupOrientation === "horizontal" ? "vertical" : "horizontal")

  return (
    <Separator
      data-slot="button-group-separator"
      orientation={resolvedOrientation}
      className={mergeClassName(
        resolvedOrientation === "horizontal"
          ? "relative z-1 mx-px w-auto self-stretch bg-input"
          : "relative z-1 my-px bg-input",
        className
      )}
      {...props}
    />
  )
}

export {
  ButtonGroup,
  ButtonGroupSeparator,
  ButtonGroupText,
  buttonGroupVariants,
}
export type {
  ButtonGroupOrientation,
  ButtonGroupProps,
  ButtonGroupSeparatorProps,
}
```

```tsx title="components/ui/separator.tsx"
"use client"

import * as React from "react"
import { Separator as SeparatorPrimitive } from "@base-ui/react/separator"
import { cn } from "cn"

type SeparatorAlign = "start" | "center" | "end"

type SeparatorOrientation = NonNullable<SeparatorPrimitive.Props["orientation"]>

type SeparatorProps = SeparatorPrimitive.Props & {
  decorative?: boolean
  align?: SeparatorAlign
}

const lineClassName: Record<SeparatorOrientation, string> = {
  horizontal:
    "h-(--hairline) w-full shrink-0 bg-border forced-colors:bg-canvas-text",
  vertical:
    "w-(--hairline) shrink-0 self-stretch bg-border forced-colors:bg-canvas-text",
}

const labelledClassName: Record<SeparatorOrientation, string> = {
  horizontal:
    "flex w-full shrink-0 items-center gap-3 text-xs text-muted-foreground before:h-(--hairline) before:min-w-4 before:flex-1 before:bg-border after:h-(--hairline) after:min-w-4 after:flex-1 after:bg-border data-[align=end]:after:hidden data-[align=start]:before:hidden",
  vertical:
    "flex shrink-0 flex-col items-center gap-3 self-stretch text-xs text-muted-foreground before:min-h-4 before:w-(--hairline) before:flex-1 before:bg-border after:min-h-4 after:w-(--hairline) after:flex-1 after:bg-border data-[align=end]:after:hidden data-[align=start]:before:hidden",
}

const labelAlignClassName: Record<SeparatorAlign, string> = {
  start: "text-start",
  center: "text-center",
  end: "text-end",
}

function mergeClassName<State>(
  base: string,
  className: string | ((state: State) => string | undefined) | undefined
) {
  return typeof className === "function"
    ? (state: State) => cn(base, className(state))
    : cn(base, className)
}

function hasContent(children: React.ReactNode) {
  return React.Children.toArray(children).some(
    (child) => typeof child !== "string" || child.trim() !== ""
  )
}

function Separator({
  className,
  orientation = "horizontal",
  decorative = false,
  align = "center",
  children,
  ...props
}: SeparatorProps) {
  const labelled = hasContent(children)
  const semantics = labelled
    ? { role: undefined, "aria-orientation": undefined }
    : decorative
      ? { role: "none", "aria-orientation": undefined }
      : {}

  return (
    <SeparatorPrimitive
      data-slot="separator"
      data-content={labelled ? "" : undefined}
      data-align={labelled ? align : undefined}
      orientation={orientation}
      className={mergeClassName(
        (labelled ? labelledClassName : lineClassName)[
          orientation === "vertical" ? "vertical" : "horizontal"
        ],
        className
      )}
      {...semantics}
      {...props}
    >
      {labelled ? (
        <span
          data-slot="separator-label"
          className={cn(
            "flex max-w-full min-w-0 items-center gap-1.5 text-pretty wrap-anywhere [&>svg]:size-3.5 [&>svg]:shrink-0",
            labelAlignClassName[align]
          )}
        >
          {children}
        </span>
      ) : null}
    </SeparatorPrimitive>
  )
}

export { Separator }
export type { SeparatorAlign, SeparatorProps }
```

Update the import paths to match your project setup.

## Usage

```tsx
import {
  ButtonGroup,
  ButtonGroupSeparator,
  ButtonGroupText,
} from "@/components/ui/button-group"
```

```tsx
<ButtonGroup aria-label="Text formatting">
  <Button variant="outline">Bold</Button>
  <Button variant="outline">Italic</Button>
  <Button variant="outline">Underline</Button>
</ButtonGroup>
```

Button group joins the buttons you already use. Install `<Button />` as well.

## Composition

```text
ButtonGroup
├── Button
├── ButtonGroupSeparator
├── ButtonGroupText
└── ButtonGroup
```

## Examples

### Sizes

Set `size` on each button. Joined corners stay square at every size, and the outer corners keep the button’s radius.

```tsx title="components/examples/button-group/sizes.tsx"
import { IconBold, IconItalic, IconUnderline } from "@tabler/icons-react"

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

const sizes = ["xs", "sm", "default", "lg"] as const

export function ButtonGroupSizes() {
  return (
    <div className="flex flex-col items-center gap-3">
      {sizes.map((size) => (
        <ButtonGroup key={size} aria-label="Formatting">
          <Button variant="outline" size={size}>
            <IconBold data-icon="inline-start" />
            Bold
          </Button>
          <Button variant="outline" size={size}>
            <IconItalic data-icon="inline-start" />
            Italic
          </Button>
          <Button variant="outline" size={size}>
            <IconUnderline data-icon="inline-start" />
            Underline
          </Button>
        </ButtonGroup>
      ))}
    </div>
  )
}
```

### Split button

Filled buttons sit flush, so add a `<ButtonGroupSeparator />` between them. A dropdown trigger can be any segment.

```tsx title="components/examples/button-group/split.tsx"
import { IconChevronDown, IconGitBranch } from "@tabler/icons-react"

import { Button } from "@/components/ui/button"
import { ButtonGroup, ButtonGroupSeparator } from "@/components/ui/button-group"
import {
  DropdownMenu,
  DropdownMenuContent,
  DropdownMenuGroup,
  DropdownMenuItem,
  DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"

export function ButtonGroupSplit() {
  return (
    <div className="flex flex-wrap items-center justify-center gap-3">
      <ButtonGroup aria-label="Deploy">
        <Button>
          <IconGitBranch data-icon="inline-start" />
          Deploy main
        </Button>
        <ButtonGroupSeparator />
        <DropdownMenu>
          <DropdownMenuTrigger
            render={<Button size="icon" aria-label="Choose branch" />}
          >
            <IconChevronDown />
          </DropdownMenuTrigger>
          <DropdownMenuContent align="end">
            <DropdownMenuGroup>
              <DropdownMenuItem>Deploy staging</DropdownMenuItem>
              <DropdownMenuItem>Deploy preview</DropdownMenuItem>
            </DropdownMenuGroup>
          </DropdownMenuContent>
        </DropdownMenu>
      </ButtonGroup>
      <ButtonGroup aria-label="Delete">
        <Button variant="destructive">Delete</Button>
        <ButtonGroupSeparator />
        <Button variant="destructive" size="icon" aria-label="More">
          <IconChevronDown />
        </Button>
      </ButtonGroup>
    </div>
  )
}
```

### Separators

Separators between icon buttons, with a toggle that reports its pressed state.

```tsx title="components/examples/button-group/player.tsx"
"use client"

import * as React from "react"
import {
  IconPlayerPause,
  IconPlayerPlay,
  IconPlayerSkipBack,
  IconPlayerSkipForward,
} from "@tabler/icons-react"

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

export function ButtonGroupPlayer() {
  const [playing, setPlaying] = React.useState(false)

  return (
    <ButtonGroup aria-label="Playback">
      <Button variant="secondary" size="icon" aria-label="Previous track">
        <IconPlayerSkipBack />
      </Button>
      <ButtonGroupSeparator />
      <Button
        variant="secondary"
        size="icon"
        aria-label={playing ? "Pause" : "Play"}
        aria-pressed={playing}
        onClick={() => setPlaying(!playing)}
      >
        {playing ? <IconPlayerPause /> : <IconPlayerPlay />}
      </Button>
      <ButtonGroupSeparator />
      <Button variant="secondary" size="icon" aria-label="Next track">
        <IconPlayerSkipForward />
      </Button>
    </ButtonGroup>
  )
}
```

### Feedback inside a group

When a button’s label changes width during loading, success or error, the segments after it slide along instead of jumping.

```tsx title="components/examples/button-group/feedback.tsx"
"use client"

import * as React from "react"
import { IconChevronDown } from "@tabler/icons-react"

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

function wait(ms: number) {
  return new Promise<void>((resolve) => setTimeout(resolve, ms))
}

async function fail(ms: number) {
  await wait(ms)
  throw new Error("Request failed")
}

export function ButtonGroupFeedback() {
  const [shouldFail, setShouldFail] = React.useState(false)

  return (
    <div className="flex flex-wrap items-center justify-center gap-3">
      <ButtonGroup aria-label="Save changes">
        <Button
          feedback
          loadingLabel="Saving changes"
          successLabel="All changes saved"
          errorLabel="Couldn't save"
          onClick={() => (shouldFail ? fail(900) : wait(900))}
        >
          Save
        </Button>
        <ButtonGroupSeparator />
        <Button size="icon" aria-label="More save options">
          <IconChevronDown />
        </Button>
      </ButtonGroup>
      <label className="flex items-center gap-2 text-sm text-muted-foreground">
        <input
          type="checkbox"
          checked={shouldFail}
          onChange={(event) => setShouldFail(event.target.checked)}
        />
        Fail next save
      </label>
    </div>
  )
}
```

### Vertical

`orientation="vertical"` stacks the segments. Separators turn horizontal automatically.

```tsx title="components/examples/button-group/vertical.tsx"
"use client"

import * as React from "react"
import { IconMinus, IconPlus } from "@tabler/icons-react"

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

export function ButtonGroupVertical() {
  const [zoom, setZoom] = React.useState(100)

  return (
    <div className="flex flex-wrap items-start justify-center gap-8">
      <div className="flex items-center gap-4">
        <ButtonGroup orientation="vertical" aria-label="Zoom">
          <Button
            variant="outline"
            size="icon"
            aria-label="Zoom in"
            disabled={zoom >= 200}
            onClick={() => setZoom((value) => Math.min(200, value + 25))}
          >
            <IconPlus />
          </Button>
          <Button
            variant="outline"
            size="icon"
            aria-label="Zoom out"
            disabled={zoom <= 25}
            onClick={() => setZoom((value) => Math.max(25, value - 25))}
          >
            <IconMinus />
          </Button>
        </ButtonGroup>
        <p className="text-sm text-muted-foreground tabular-nums">{zoom}%</p>
      </div>
      <ButtonGroup orientation="vertical" aria-label="Sort">
        <Button variant="outline">Newest first</Button>
        <Button variant="outline">Oldest first</Button>
        <Button variant="outline">Most discussed</Button>
      </ButtonGroup>
      <ButtonGroup orientation="vertical" aria-label="Position">
        <Button variant="secondary">Top</Button>
        <ButtonGroupSeparator />
        <Button variant="secondary">Bottom</Button>
      </ButtonGroup>
    </div>
  )
}
```

### Nested groups

Groups inside a group become separate clusters with a gap between them, each joined on its own.

```tsx title="components/examples/button-group/nested.tsx"
"use client"

import * as React from "react"
import { IconArrowLeft, IconArrowRight } from "@tabler/icons-react"

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

const pages = [1, 2, 3, 4]

export function ButtonGroupNested() {
  const [page, setPage] = React.useState(1)

  return (
    <ButtonGroup aria-label="Pagination">
      <ButtonGroup>
        <Button
          variant="outline"
          size="icon"
          aria-label="Previous page"
          disabled={page === 1}
          onClick={() => setPage(page - 1)}
        >
          <IconArrowLeft className="rtl:-scale-x-100" />
        </Button>
        <Button
          variant="outline"
          size="icon"
          aria-label="Next page"
          disabled={page === pages.length}
          onClick={() => setPage(page + 1)}
        >
          <IconArrowRight className="rtl:-scale-x-100" />
        </Button>
      </ButtonGroup>
      <ButtonGroup>
        {pages.map((value) => (
          <Button
            key={value}
            variant={value === page ? "secondary" : "outline"}
            size="icon"
            aria-current={value === page ? "page" : undefined}
            onClick={() => setPage(value)}
          >
            {value}
          </Button>
        ))}
      </ButtonGroup>
    </ButtonGroup>
  )
}
```

### With text and an input

`<ButtonGroupText />` adds a label or prefix. Render it as a `<label>` to name the input. The input stretches to fill the row.

```tsx title="components/examples/button-group/input.tsx"
"use client"

import * as React from "react"
import { IconCopy, IconLink } from "@tabler/icons-react"

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

export function ButtonGroupInput() {
  const [value, setValue] = React.useState("hextaui.com/docs/button-group")

  return (
    <ButtonGroup className="w-full max-w-md" aria-label="Share link">
      <ButtonGroupText render={<label htmlFor="share-url" />}>
        <IconLink />
        https://
      </ButtonGroupText>
      <input
        id="share-url"
        value={value}
        onChange={(event) => setValue(event.target.value)}
        className="h-9 rounded-md border border-input bg-background px-3 text-sm transition-shadow outline-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-focus-ring focus-visible:outline-hidden dark:bg-input/30 pointer-coarse:text-touch"
      />
      <Button
        variant="outline"
        feedback
        successLabel="Copied"
        errorLabel="Blocked"
        onClick={() => navigator.clipboard.writeText(`https://${value}`)}
      >
        <IconCopy data-icon="inline-start" />
        Copy
      </Button>
    </ButtonGroup>
  )
}
```

### Disabled and invalid

Disable or mark a single segment with `aria-invalid`. An invalid segment rises above its neighbours so its red ring is never covered.

```tsx title="components/examples/button-group/disabled.tsx"
import { Button } from "@/components/ui/button"
import { ButtonGroup } from "@/components/ui/button-group"

export function ButtonGroupDisabled() {
  return (
    <div className="flex flex-wrap items-center justify-center gap-3">
      <ButtonGroup aria-label="Post actions">
        <Button variant="outline">Edit</Button>
        <Button variant="outline" disabled>
          Publish
        </Button>
        <Button variant="outline">Share</Button>
      </ButtonGroup>
      <ButtonGroup aria-label="Date range">
        <Button variant="outline">Start</Button>
        <Button variant="outline" aria-invalid>
          Missing date
        </Button>
        <Button variant="outline">End</Button>
      </ButtonGroup>
    </div>
  )
}
```

### Full width

Give the group a width and each button `flex-1` to split the space evenly.

```tsx title="components/examples/button-group/full-width.tsx"
import { Button } from "@/components/ui/button"
import { ButtonGroup } from "@/components/ui/button-group"

export function ButtonGroupFullWidth() {
  return (
    <ButtonGroup className="w-full max-w-md" aria-label="Range">
      <Button variant="outline" className="flex-1">
        Day
      </Button>
      <Button variant="outline" className="flex-1">
        Week
      </Button>
      <Button variant="outline" className="flex-1">
        Month
      </Button>
    </ButtonGroup>
  )
}
```

### As a fieldset

Use `render` to output a `<fieldset>` inside forms.

```tsx title="components/examples/button-group/fieldset.tsx"
import { Button } from "@/components/ui/button"
import { ButtonGroup } from "@/components/ui/button-group"

export function ButtonGroupFieldset() {
  return (
    <ButtonGroup render={<fieldset />} aria-label="Reply">
      <Button variant="outline">Reply</Button>
      <Button variant="outline">Reply all</Button>
      <Button variant="outline">Forward</Button>
    </ButtonGroup>
  )
}
```

### Right to left

Joined corners, separators and nested clusters mirror in right-to-left layouts.

```tsx title="components/examples/button-group/rtl.tsx"
"use client"

import * as React from "react"
import {
  IconArchive,
  IconArrowLeft,
  IconArrowRight,
  IconChevronDown,
  IconDotsVertical,
} from "@tabler/icons-react"

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

const pages = [1, 2, 3]

export function ButtonGroupRtl() {
  const [page, setPage] = React.useState(1)

  return (
    <div dir="rtl" className="flex flex-col items-center gap-3">
      <ButtonGroup aria-label="إجراءات">
        <Button variant="outline">
          <IconArchive data-icon="inline-start" />
          أرشفة
        </Button>
        <Button variant="outline">إبلاغ</Button>
        <Button variant="outline" size="icon" aria-label="المزيد">
          <IconDotsVertical />
        </Button>
      </ButtonGroup>
      <ButtonGroup aria-label="نشر">
        <Button>نشر الآن</Button>
        <ButtonGroupSeparator />
        <Button size="icon" aria-label="خيارات">
          <IconChevronDown />
        </Button>
      </ButtonGroup>
      <ButtonGroup aria-label="الصفحات">
        <ButtonGroup>
          <Button
            variant="outline"
            size="icon"
            aria-label="السابق"
            disabled={page === 1}
            onClick={() => setPage(page - 1)}
          >
            <IconArrowLeft className="rtl:-scale-x-100" />
          </Button>
          <Button
            variant="outline"
            size="icon"
            aria-label="التالي"
            disabled={page === pages.length}
            onClick={() => setPage(page + 1)}
          >
            <IconArrowRight className="rtl:-scale-x-100" />
          </Button>
        </ButtonGroup>
        <ButtonGroup>
          {pages.map((value) => (
            <Button
              key={value}
              variant={value === page ? "secondary" : "outline"}
              size="icon"
              aria-current={value === page ? "page" : undefined}
              onClick={() => setPage(value)}
            >
              {value}
            </Button>
          ))}
        </ButtonGroup>
      </ButtonGroup>
    </div>
  )
}
```

## Keyboard

| Key | Action |
| --- | --- |
| `Tab` | Moves through the segments in order. The focused segment rises so its ring and border are never covered. |
| `Enter` `Space` | Activates the focused button. |

## Accessibility

- The group has `role="group"`. Give it an `aria-label` that describes the set, like “Message actions”.
- Icon-only segments need their own `aria-label`.
- Separators are exposed as separators with the right orientation. Add `decorative` when the line is only visual.

## API reference

Every part renders a `<div>` and accepts its attributes.

### ButtonGroup

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `orientation` | `"horizontal" \| "vertical"` | `"horizontal"` |  |
| `render` | `ReactElement \| (props, state) => ReactElement` | `<div>` |  |

| Attribute | Description |
| --- | --- |
| `data-slot="button-group"` | Target groups in CSS. |
| `data-orientation` | The current orientation. |

### ButtonGroupSeparator

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `orientation` | `"horizontal" \| "vertical"` | – | Defaults to the opposite of the group’s orientation. |
| `decorative` | `boolean` | `false` | Hide the line from screen readers. |
| `render` | `ReactElement \| (props, state) => ReactElement` | `<div>` |  |

| Attribute | Description |
| --- | --- |
| `data-slot="button-group-separator"` | Target separators in CSS. |
| `data-orientation` | The resolved orientation. |

### ButtonGroupText

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `render` | `ReactElement \| (props, state) => ReactElement` | `<div>` | Render a <label> to name an input in the group. |

| Attribute | Description |
| --- | --- |
| `data-slot="button-group-text"` | Target text segments in CSS. |

## Notes for AI assistants

- Install a component with the shadcn CLI: `npx shadcn@latest add https://hextaui.com/r/<name>.json`. It adds the source, the HextaUI theme tokens and any HextaUI components it depends on. `https://hextaui.com/r/all.json` installs every component.
- The code is then owned by the project, like shadcn/ui. There is no HextaUI npm package. HextaUI is MIT licensed and free for personal and commercial use.
- Behavior and accessibility come from Base UI (`@base-ui/react`). Compose with the `render` prop, not `asChild`.
- Styling uses Tailwind CSS v4 with theme tokens. Merge classes with `cn` from the `cn` package.
- Icons come from `@tabler/icons-react`.
- Import components from `@/components/ui/<name>`, hooks from `@/hooks/<name>` and utilities from `@/lib/<name>`.

Every HextaUI doc: https://hextaui.com/llms.txt
