# Group

Join related controls into one visual group.

Message actions with an overflow menu

[Complete TSX file](/examples/group/default.tsx)

```tsx
"use client";

import {
  ArchiveIcon,
  CalendarPlusIcon,
  ClockIcon,
  EllipsisIcon,
  FlagIcon,
  MailCheckIcon,
  Trash2Icon,
} from "lucide-react";
import { Button } from "@/siteplane/components/button";
import { Group, GroupSeparator } from "@/siteplane/components/group";
import {
  Menu,
  MenuItem,
  MenuPopup,
  MenuSeparator,
  MenuTrigger,
} from "@/siteplane/components/menu";

export default function Example() {
  return (
    <Group aria-label="Message actions">
      <Button variant="outline">
        <ArchiveIcon />
        Archive
      </Button>
      <GroupSeparator />
      <Button variant="outline">
        <FlagIcon />
        Report
      </Button>
      <GroupSeparator />
      <Menu>
        <MenuTrigger
          render={
            <Button
              aria-label="More actions"
              shape="square"
              variant="outline"
            />
          }
        >
          <EllipsisIcon />
        </MenuTrigger>
        <MenuPopup align="end">
          <MenuItem>
            <MailCheckIcon />
            Mark as read
          </MenuItem>
          <MenuItem>
            <ClockIcon />
            Snooze
          </MenuItem>
          <MenuItem>
            <CalendarPlusIcon />
            Add to calendar
          </MenuItem>
          <MenuSeparator />
          <MenuItem variant="destructive">
            <Trash2Icon />
            Delete
          </MenuItem>
        </MenuPopup>
      </Menu>
    </Group>
  );
}
```

## Installation

Complete [one-time setup](/docs/installation/) first.

```bash
pnpm dlx shadcn@4.18.0 add @siteplane/group
```

With npm: `npx shadcn@4.18.0 add @siteplane/group`. [Registry JSON](/registry/group.json) lists source files and dependencies.

For this page’s examples, also install:

```bash
pnpm dlx shadcn@4.18.0 add @siteplane/button @siteplane/form @siteplane/input @siteplane/menu
```

## Usage

```tsx
import { Button } from "@/siteplane/components/button";
import { Group, GroupSeparator } from "@/siteplane/components/group";

<Group aria-label="Message actions">
  <Button variant="outline">Archive</Button>
  <GroupSeparator />
  <Button variant="outline">Report</Button>
</Group>;
```

## Composition

```text
Group
├── GroupText
├── Input or Button
├── GroupSeparator
└── Button
```

## With text

Use `GroupText` for a fixed prefix or suffix, such as a protocol or unit. Next to an input, the group draws one frame around the text and the input, with the same focus, invalid and disabled states as an input. `GroupText` doesn't label the input, so give the input its own accessible name.

Website address with a prefix

[Complete TSX file](/examples/group/text.tsx)

```tsx
"use client";

import { Group, GroupSeparator, GroupText } from "@/siteplane/components/group";
import { Input } from "@/siteplane/components/input";

export default function Example() {
  return (
    <Group aria-label="Website address" className="w-full max-w-sm">
      <GroupText>https://</GroupText>
      <GroupSeparator />
      <Input aria-label="Domain" placeholder="example.com" type="text" />
    </Group>
  );
}
```

## With input

Join an input with the button that acts on its value. Put the group in a `Form` and give the button `type="submit"`: Enter in the input then submits the form like a click on the button, and `onSubmit` receives both. A keyboard submit does not press the button, so the confirmation belongs to the result, shown here as a status line. While a request runs, set `loading` on the button.

Search form with an input and a submit button

[Complete TSX file](/examples/group/input-button.tsx)

```tsx
"use client";

import { SearchIcon } from "lucide-react";
import { useState } from "react";
import { Button } from "@/siteplane/components/button";
import { Form } from "@/siteplane/components/form";
import { Group } from "@/siteplane/components/group";
import { Input } from "@/siteplane/components/input";

export default function Example() {
  const [query, setQuery] = useState("");

  return (
    <Form
      className="grid w-full max-w-sm gap-2"
      onSubmit={(event) => {
        event.preventDefault();
        const data = new FormData(event.currentTarget);
        setQuery(String(data.get("query") ?? "").trim());
      }}
    >
      <Group aria-label="Project search" className="w-full">
        <Input
          aria-label="Search projects"
          name="query"
          placeholder="Search projects"
          type="search"
        />
        <Button type="submit">
          <SearchIcon />
          Search
        </Button>
      </Group>
      <p className="min-h-5 text-muted-foreground text-sm" role="status">
        {query && `Searching for “${query}”`}
      </p>
    </Form>
  );
}
```

## Orientation

Set `orientation="vertical"` to stack controls; a `GroupSeparator` between them turns horizontal with the group.

Vertical zoom controls

[Complete TSX file](/examples/group/vertical.tsx)

```tsx
"use client";

import { ZoomInIcon, ZoomOutIcon } from "lucide-react";
import { Button } from "@/siteplane/components/button";
import { Group, GroupSeparator } from "@/siteplane/components/group";

export default function Example() {
  return (
    <Group aria-label="Zoom controls" orientation="vertical">
      <Button aria-label="Zoom in" size="icon-md" variant="outline">
        <ZoomInIcon />
      </Button>
      <GroupSeparator />
      <Button aria-label="Zoom out" size="icon-md" variant="outline">
        <ZoomOutIcon />
      </Button>
    </Group>
  );
}
```

## API reference

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| Group.orientation | horizontal \| vertical | horizontal styling | Joined edges; data-orientation omitted unless set. |
| Group.separator | inset | unset | Separates direct buttons of a horizontal group with a short inset divider instead of a full-height line. Use GroupSeparator between other controls. |
| Group.children | ReactNode | required | Controls or nested groups. |
| Group.role | ARIA role | group | Grouping semantics. |
| GroupText.render | ReactElement \| render function | div | Composed element. |
| GroupSeparator.orientation | horizontal \| vertical | across the group | Divider direction: vertical in a horizontal group, horizontal in a vertical one. |
Group accepts native div props and refs; GroupText accepts useRender div props; GroupSeparator accepts Separator props. All accept string className. `groupVariants({orientation, separator, className})` returns styling, defaulting orientation to horizontal. No shared state, size, disabled or roving focus API is added.

[Read the installed source](/source/group).

## Usage notes

Label every group with `aria-label` or `aria-labelledby`. Use Toolbar for roving focus and ToggleGroup for shared pressed selection. Controls keep their own behavior and focus stops.

Give an icon-only segment `shape="square"`, including a trigger supplied through `render`, so it takes the same height as the buttons beside it. A fixed icon size such as `size="icon-md"` keeps its own height: next to buttons that follow the provider's control size, its hover surface covers only part of the segment and its icon sits off center.
