1
ComponentsLayout

Group

Group joins related controls into one visual cluster with shared borders and connected radii.

Purpose

Group joins related controls into one visual cluster with shared borders and connected radii. Use ordinary flex or grid for unrelated actions or layout.

Merged controls

https://
import { Group, GroupSeparator, GroupText } from "@/components/ui/group";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";

export function Example() {
  return (
    <Group>
      <GroupText>https://</GroupText>
      <Input aria-label="URL" />
      <GroupSeparator />
      <Button>Copy</Button>
    </Group>
  );
}

Installation

With the Siteplane registry alias configured, add the component and import the installed source from your app.

CLI

pnpm dlx shadcn@4.16.1 add @siteplane/group

Import

import { Group, GroupSeparator, GroupText } from "@/components/ui/group";

Need the machine-readable source metadata? View registry JSON.

Usage

Group merges adjacent controls so borders, radii and focus states read as one control cluster.

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

export function Example() {
  return (
    <Group>
      <GroupText>https://</GroupText>
      <Input aria-label="URL" />
      <GroupSeparator />
      <Button>Copy</Button>
    </Group>
  );
}

Examples

Button clusters, separators, orientation and text affixes are props and compositions of the same primitive.

Button group

<Group aria-label="Text alignment">
  <Button aria-label="Align left" size="icon-sm" variant="outline">
    <AlignLeftIcon />
  </Button>
  <Button aria-label="Align center" size="icon-sm" variant="outline">
    <AlignCenterIcon />
  </Button>
  <Button aria-label="Align right" size="icon-sm" variant="outline">
    <AlignRightIcon />
  </Button>
</Group>

With separator

<Group aria-label="History controls">
  <Button variant="outline">Undo</Button>
  <Button variant="outline">Redo</Button>
  <GroupSeparator />
  <Button variant="outline">Reset</Button>
</Group>

Vertical

<Group aria-label="Page navigation" orientation="vertical">
  <Button variant="outline">Top</Button>
  <Button variant="outline">Middle</Button>
  <Button variant="outline">Bottom</Button>
</Group>

Prefix and suffix

https://
.ui.siteplane.io
<Group aria-label="Project subdomain">
  <GroupText>https://</GroupText>
  <Input aria-label="Subdomain" placeholder="acme" />
  <GroupText>.ui.siteplane.io</GroupText>
</Group>

Input with button

Query: not submitted
const [loading, setLoading] = useState(false);
const [status, setStatus] = useState("Query: not submitted");

async function onSubmit(event: FormEvent<HTMLFormElement>) {
  event.preventDefault();
  const formData = new FormData(event.currentTarget);
  setLoading(true);
  await new Promise((resolve) => setTimeout(resolve, 800));
  setLoading(false);
  setStatus(`Query: ${String(formData.get("q") || "empty")}`);
}

<form onSubmit={onSubmit}>
  <Group aria-label="Search projects">
    <Input aria-label="Search projects" name="q" placeholder="Search projects" type="search" />
    <Button loading={loading} type="submit">
      <SearchIcon />
      Search
    </Button>
  </Group>
  <span className="text-muted-foreground text-xs">{status}</span>
</form>

Segmented actions

Action: none
<Group aria-label="Publish actions">
  <Button onClick={() => setStatus("Action: publish")} variant="outline">
    Publish
  </Button>
  <Button
    aria-label="Share"
    onClick={() => setStatus("Action: share")}
    size="icon-sm"
    variant="outline"
  >
    <ShareIcon />
  </Button>
</Group>

Filter group

Toggle-style filters keep the pressed state on the buttons, not on the group.

<Group aria-label="Time range filter">
  {ranges.map((item) => (
    <Button
      aria-pressed={range === item.value}
      className={cn(range === item.value && "border-input")}
      key={item.value}
      onClick={() => setRange(item.value)}
      variant={range === item.value ? "secondary" : "outline"}
    >
      {item.label}
    </Button>
  ))}
</Group>

API Reference

The reference lists the source-owned exports and props that are easy to miss in visual examples. Inherited Base UI props remain available unless the wrapper narrows them.

  • orientation accepts "horizontal" or "vertical".
  • Match GroupSeparator orientation to the divider itself. A vertical Group needs orientation="horizontal" separators between rows.
  • GroupText is a non-interactive prefix, suffix, or status segment.
  • Nested direct-child Groups remain separate clusters with the native gap, allowing a toolbar to contain several connected control sets.

Motion

Component motion uses the shared Siteplane motion contract. See the global motion guide for provider setup, tokens and reduced-motion behavior.

  • Group does not animate. Children retain their native interaction and reduced-motion behavior.
  • Connected borders, radii, focus rings, and invalid state come from Group.
  • Do not add transforms to the entire cluster.

Accessibility

  • Name every meaningful control group through aria-label or aria-labelledby.
  • Every icon-only Button needs its own accessible label.
  • Toggle-like controls retain aria-pressed or their native selection semantics.
  • GroupText is supporting text, never a control.

Implementation Guidance

  • Group only visually connects controls that belong to one task.
  • Use matching separator orientation and nested Groups for multiple clusters.
  • Do not use Group as a replacement for Toolbar semantics or general layout.

On This Page