Siteplane UI

Group

Join related controls into one visual group.

Markdown
"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

pnpm dlx shadcn@4.18.0 add @siteplane/group

Complete the one-time setup first. Registry JSON lists source files and dependencies.

For this page’s examples, also install:

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

Usage

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

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.

https://
"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.

"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.

"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

Component properties
PropTypeDefaultDescription
Group.orientationhorizontal | verticalhorizontal stylingJoined edges; data-orientation omitted unless set.
Group.separatorinsetunsetSeparates direct buttons of a horizontal group with a short inset divider instead of a full-height line. Use GroupSeparator between other controls.
Group.childrenReactNoderequiredControls or nested groups.
Group.roleARIA rolegroupGrouping semantics.
GroupText.renderReactElement | render functiondivComposed element.
GroupSeparator.orientationhorizontal | verticalacross the groupDivider 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.

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.