component

Group

Joins related controls — inputs, buttons, checkboxes, radios, badges — into one connected unit. It uses the children’s own borders: the corners where they meet are flattened and adjacent borders overlap into a single seam, so the group reads as one element with no double border and no frame of its own. Need a real bordered/padded container around the joined children? Opt in with the Panel chrome props (border, padding, background, emphasis, …). For overlapping avatars, use variant="stack".

Basic

The default controls variant joins inputs, buttons, checkboxes, and radios into a single connected element by flattening the inner corners and merging each child’s own border with its neighbour’s.

Full width — search bar

Full width — segmented control

Input with button

Button on both sides

Multiple inputs

Button group

Mixed feedback buttons

Tab horizontal

Tab vertical

Vertical button group

Vertical checkbox group

Vertical radio button group

Checkbox button group

Radio button group

---
import Group from "../Group.astro";
import Input from "@pindoba/astro-input";
import Button from "@pindoba/astro-button";
import Checkbox from "@pindoba/astro-checkbox";
import Radio from "@pindoba/astro-radio";
import { stack } from "@pindoba/styled-system/patterns";
---

<div class={stack({ gap: "xl", direction: "column", width: "full" })}>
  <div>
    <h3>Full width — search bar</h3>
    <Group variant="controls" fullWidth>
      <Input fullWidth placeholder="Search..." />
      <Button>Search</Button>
    </Group>
  </div>

  <div>
    <h3>Full width — segmented control</h3>
    <Group variant="controls" fullWidth>
      <Radio
        id="group-fw-env-dev"
        appearance="button"
        name="group-fw-env"
        value="dev"
        fullWidth
        checked>Dev</Radio
      >
      <Radio
        id="group-fw-env-staging"
        appearance="button"
        name="group-fw-env"
        value="staging"
        fullWidth>Staging</Radio
      >
      <Radio
        id="group-fw-env-prod"
        appearance="button"
        name="group-fw-env"
        value="prod"
        fullWidth>Prod</Radio
      >
    </Group>
  </div>

  <div>
    <h3>Input with button</h3>
    <Group variant="controls">
      <Input placeholder="Search..." />
      <Button>Search</Button>
    </Group>
  </div>

  <div>
    <h3>Button on both sides</h3>
    <Group variant="controls">
      <Button emphasis="secondary">-</Button>
      <Input placeholder="Enter value" />
      <Button emphasis="secondary">+</Button>
    </Group>
  </div>

  <div>
    <h3>Multiple inputs</h3>
    <Group variant="controls">
      <Input placeholder="First name" />
      <Input placeholder="Last name" />
    </Group>
  </div>

  <div>
    <h3>Button group</h3>
    <Group variant="controls">
      <Button emphasis="secondary">Cut</Button>
      <Button emphasis="secondary">Copy</Button>
      <Button emphasis="secondary">Paste</Button>
    </Group>
  </div>

  <div>
    <h3>Mixed feedback buttons</h3>
    <Group variant="controls">
      <Button feedback="success">Save</Button>
      <Button feedback="warning" emphasis="secondary">Draft</Button>
      <Button feedback="danger" emphasis="secondary">Delete</Button>
    </Group>
  </div>

  <div>
    <h3>Tab horizontal</h3>
    <Group variant="controls">
      <Radio
        id="group-tab-h-all"
        appearance="tab"
        name="group-tab-h"
        value="all"
        checked>All</Radio
      >
      <Radio
        id="group-tab-h-active"
        appearance="tab"
        name="group-tab-h"
        value="active">Active</Radio
      >
      <Radio
        id="group-tab-h-archived"
        appearance="tab"
        name="group-tab-h"
        value="archived">Archived</Radio
      >
    </Group>
  </div>

  <div>
    <h3>Tab vertical</h3>
    <Group variant="controls" orientation="vertical">
      <Radio
        id="group-tab-v-dashboard"
        appearance="tab"
        orientation="vertical"
        name="group-tab-v"
        value="dashboard"
        checked>Dashboard</Radio
      >
      <Radio
        id="group-tab-v-settings"
        appearance="tab"
        orientation="vertical"
        name="group-tab-v"
        value="settings">Settings</Radio
      >
      <Radio
        id="group-tab-v-profile"
        appearance="tab"
        orientation="vertical"
        name="group-tab-v"
        value="profile">Profile</Radio
      >
    </Group>
  </div>

  <div>
    <h3>Vertical button group</h3>
    <Group variant="controls" orientation="vertical">
      <Button emphasis="secondary">Cut</Button>
      <Button emphasis="secondary">Copy</Button>
      <Button emphasis="secondary">Paste</Button>
    </Group>
  </div>

  <div>
    <h3>Vertical checkbox group</h3>
    <Group variant="controls" orientation="vertical">
      <Checkbox appearance="button" size="md">Bold</Checkbox>
      <Checkbox appearance="button" size="md">Italic</Checkbox>
      <Checkbox appearance="button" size="md">Underline</Checkbox>
    </Group>
  </div>

  <div>
    <h3>Vertical radio button group</h3>
    <Group variant="controls" orientation="vertical">
      <Radio
        id="group-view-v-list"
        appearance="button"
        size="md"
        name="view-v"
        value="list">List</Radio
      >
      <Radio
        id="group-view-v-grid"
        appearance="button"
        size="md"
        name="view-v"
        value="grid">Grid</Radio
      >
      <Radio
        id="group-view-v-table"
        appearance="button"
        size="md"
        name="view-v"
        value="table">Table</Radio
      >
    </Group>
  </div>

  <div>
    <h3>Checkbox button group</h3>
    <Group variant="controls">
      <Checkbox appearance="button" size="md">Bold</Checkbox>
      <Checkbox appearance="button" size="md">Italic</Checkbox>
      <Checkbox appearance="button" size="md">Underline</Checkbox>
    </Group>
  </div>

  <div>
    <h3>Radio button group</h3>
    <Group variant="controls">
      <Radio
        id="group-view-list"
        appearance="button"
        size="md"
        name="view"
        value="list">List</Radio
      >
      <Radio
        id="group-view-grid"
        appearance="button"
        size="md"
        name="view"
        value="grid">Grid</Radio
      >
      <Radio
        id="group-view-table"
        appearance="button"
        size="md"
        name="view"
        value="table">Table</Radio
      >
    </Group>
  </div>
</div>

Feedback

With variant="controls", apply a semantic color to the merged frame and pass matching feedback to the inner components.

---
import Group from "../Group.astro";
import Input from "@pindoba/astro-input";
import Button from "@pindoba/astro-button";
import { stack } from "@pindoba/styled-system/patterns";
---

<div class={stack({ gap: "md", direction: "column", width: "full" })}>
  <Group variant="controls" feedback="neutral">
    <Input placeholder="Neutral" />
    <Button>Go</Button>
  </Group>

  <Group variant="controls" feedback="primary">
    <Input feedback="primary" placeholder="Primary" />
    <Button>Go</Button>
  </Group>

  <Group variant="controls" feedback="success">
    <Input feedback="success" placeholder="Success" />
    <Button feedback="success">Go</Button>
  </Group>

  <Group variant="controls" feedback="danger">
    <Input feedback="danger" placeholder="Danger" />
    <Button feedback="danger">Go</Button>
  </Group>

  <Group variant="controls" feedback="warning">
    <Input feedback="warning" placeholder="Warning" />
    <Button feedback="warning">Go</Button>
  </Group>
</div>

Size

Use size to control the merged frame’s border radius (controls). Match it to the size of the inner controls so the rounding is proportional.

---
import Group from "../Group.astro";
import Input from "@pindoba/astro-input";
import Button from "@pindoba/astro-button";
import { stack } from "@pindoba/styled-system/patterns";
---

<div class={stack({ gap: "md", direction: "column", width: "full" })}>
  <Group variant="controls" size="xs">
    <Input size="xs" placeholder="Extra small" />
    <Button size="xs">Go</Button>
  </Group>

  <Group variant="controls" size="sm">
    <Input size="sm" placeholder="Small" />
    <Button size="sm">Go</Button>
  </Group>

  <Group variant="controls" size="md">
    <Input size="md" placeholder="Medium" />
    <Button size="md">Go</Button>
  </Group>

  <Group variant="controls" size="lg">
    <Input size="lg" placeholder="Large" />
    <Button size="lg">Go</Button>
  </Group>
</div>

Badges and buttons

controls also merges badges — and a trailing button — into a single chip. Mix emphasis (primary / secondary / tertiary) and feedback colors freely; Group suppresses each badge’s own outline and emits a palette-aware divider between siblings. Pair button sizes with the badge size (sm badge → xs button, md badge → sm button) so nothing renders at the default button height.

React18 issues
v2.1Released
Filter12 results
Pending3 in queue
PR#247merged
CIfailedretry in 30s
Topicdesign-systems
Author@ed
Draft12 files
---
import Badge from "@pindoba/astro-badge";
import Group from "../Group.astro";
import Button from "@pindoba/astro-button";
import { X } from "@lucide/astro";
import { stack } from "@pindoba/styled-system/patterns";
---

<div class={stack({ gap: "xl", direction: "column" })}>
  <!-- Two-emphasis chips: mix primary / secondary / tertiary / adaptive so
       the same composition can play every visual role the old label slot did. -->
  <div
    class={stack({
      gap: "md",
      direction: "row",
      align: "center",
      flexWrap: "wrap",
    })}
  >
    <Group variant="controls">
      <Badge feedback="primary" emphasis="primary">React</Badge>
      <Badge feedback="primary" emphasis="secondary">18 issues</Badge>
    </Group>
    <Group variant="controls">
      <Badge feedback="success" emphasis="secondary">v2.1</Badge>
      <Badge feedback="success" emphasis="primary">Released</Badge>
    </Group>
    <Group variant="controls">
      <Badge feedback="neutral" emphasis="tertiary">Filter</Badge>
      <Badge feedback="primary" emphasis="secondary">12 results</Badge>
    </Group>
    <Group variant="controls">
      <Badge feedback="warning" emphasis="primary">Pending</Badge>
      <Badge feedback="warning" emphasis="tertiary">3 in queue</Badge>
    </Group>
  </div>

  <!-- Three-section chips: emphasis cascades across the group. -->
  <div
    class={stack({
      gap: "md",
      direction: "row",
      align: "center",
      flexWrap: "wrap",
    })}
  >
    <Group variant="controls">
      <Badge feedback="primary" emphasis="primary">PR</Badge>
      <Badge feedback="primary" emphasis="secondary">#247</Badge>
      <Badge feedback="success" emphasis="tertiary">merged</Badge>
    </Group>
    <Group variant="controls">
      <Badge feedback="danger" emphasis="primary">CI</Badge>
      <Badge feedback="danger" emphasis="secondary">failed</Badge>
      <Badge feedback="neutral" emphasis="tertiary">retry in 30s</Badge>
    </Group>
  </div>

  <!-- Badges + a trailing dismiss button. Button size matches the badge size
       (sm-badge ↔ xs-button, md-badge ↔ sm-button) so nothing renders at the
       default 30 px button height. -->
  <div
    class={stack({
      gap: "md",
      direction: "row",
      align: "center",
      flexWrap: "wrap",
    })}
  >
    <Group variant="controls" size="md">
      <Badge feedback="primary" emphasis="primary">Topic</Badge>
      <Badge feedback="primary" emphasis="secondary">design-systems</Badge>
      <Button size="sm" shape="square" emphasis="ghost"><X /></Button>
    </Group>
    <Group variant="controls" size="sm">
      <Badge size="sm" feedback="neutral" emphasis="tertiary">Author</Badge>
      <Badge size="sm" feedback="primary" emphasis="secondary">@ed</Badge>
      <Button size="xs" shape="square" emphasis="secondary"><X /></Button>
    </Group>
    <Group variant="controls" size="md">
      <Badge feedback="warning" emphasis="primary">Draft</Badge>
      <Badge feedback="neutral" emphasis="tertiary">12 files</Badge>
      <Button size="sm" shape="square" emphasis="ghost"><X /></Button>
    </Group>
  </div>
</div>

Keyboard shortcuts

controls also fuses keycaps into a single shortcut cluster — each Kbd keeps its own base edge while the shared borders collapse into one seam. Match the group size to the kbd size so the corner rounding stays proportional.

K
P
Space
CtrlAltDel
---
import Kbd from "@pindoba/astro-kbd";
import Group from "../Group.astro";
import { stack } from "@pindoba/styled-system/patterns";
---

<div
  class={stack({
    gap: "lg",
    direction: "row",
    align: "center",
    flexWrap: "wrap",
  })}
>
  <Group>
    <Kbd></Kbd>
    <Kbd>K</Kbd>
  </Group>

  <Group>
    <Kbd></Kbd>
    <Kbd></Kbd>
    <Kbd>P</Kbd>
  </Group>

  <Group size="lg">
    <Kbd size="lg"></Kbd>
    <Kbd size="lg">Space</Kbd>
  </Group>

  <Group size="sm">
    <Kbd size="sm">Ctrl</Kbd>
    <Kbd size="sm">Alt</Kbd>
    <Kbd size="sm">Del</Kbd>
  </Group>
</div>

Custom Styling

The passThrough prop provides an escape hatch for the root element. The last example shows the exposed Panel chrome props turning the plain group into a bordered, padded, filled container.

Custom style via passThrough

Custom props via passThrough

Panel chrome — bordered, padded, filled container

---
import Group from "../Group.astro";
import Input from "@pindoba/astro-input";
import Button from "@pindoba/astro-button";
import { stack } from "@pindoba/styled-system/patterns";
---

<div class={stack({ gap: "xl", direction: "column" })}>
  <div>
    <h3>Custom style via passThrough</h3>
    <Group
      variant="controls"
      passThrough={{ root: { style: { borderRadius: "full" } } }}
    >
      <Input
        placeholder="Search..."
        passThrough={{ root: { style: { borderRadius: "full" } } }}
      />
      <Button shape="pill">Go</Button>
    </Group>
  </div>

  <div>
    <h3>Custom props via passThrough</h3>
    <Group
      variant="controls"
      passThrough={{
        root: {
          props: { "data-testid": "search-group" },
        },
      }}
    >
      <Input placeholder="Search..." />
      <Button>Search</Button>
    </Group>
  </div>

  <div>
    <h3>Panel chrome — bordered, padded, filled container</h3>
    <Group border="default" padding="md" background="surface.peak">
      <Button>Save</Button>
      <Button emphasis="secondary">Cancel</Button>
    </Group>
  </div>
</div>
props · 19 shown · 19 total
background
"surface.peak""surface.hill""surface.base""surface.valley""surface.ground""transparent"
default "transparent"

Panel surface background of the group container.

border
"none""bold""default""muted""accent"
default "none"

Panel chrome — opt into a bordered/padded/filled container drawn AROUND the joined children. By default Group is chrome-less (the outline is the children's own borders).

borderInteract
"none""bold""default""muted""accent"

Panel interactive border treatment applied to the opt-in container frame.

children slot svelte
Snippet

No description yet.

class
string

Additional class name(s) merged onto the group's root element.

element binding svelte
HTMLDivElementnull

No description yet.

emphasis
"primary""secondary""tertiary"

Panel emphasis (primary / secondary / tertiary) for the opt-in container frame.

feedback
"primary""neutral""success""warning""danger""inherit"
default "neutral"

Semantic color palette for the group, inherited by children and any opt-in frame.

fullWidth
boolean
default false

Stretch the group to fill its container's width.

interactive
boolean

Enable Panel interactive (hover/press) affordances on the container frame.

orientation
"horizontal""vertical"
default "horizontal"

Lay children out in a row or a column.

padding
"sm""md""lg""xl""2xl""none""xs""3xl""4xl""5xl""6xl""7xl""8xl""4xs""3xs""2xs""9xl""10xl""11xl"
default "none"

Panel padding — inner spacing of the group container.

passThrough
GroupPassThrough<RootElementAttributes>

Per-slot style and attribute override bag for the group's root element.

radius
"sm""md""lg""xl""2xl""none""xs""3xl""4xl""5xl""6xl""full""2xs""inner""inherit"

Corner radius of the framed container. By default the radius is owned by the `size` variant (so joined children nest concentrically). Pass an explicit `radius` to override that size-derived default — useful when the Group frames a chrome'd container (e.g. a segmented nav) that must match a sibling's radius rather than its own size scale.

reserveBorderSpace
boolean
default true

Opt out of the Panel's reserved 1px border footprint. The default keeps the reserved space so toggling border variants never shifts layout; set `false` on a chrome-less group that must not add 1px around the joined set (e.g. a structural join-group nested inside a parent that already owns the frame).

shadow
"sm""md""lg""xl""none""xs"

Panel shadow elevation applied to the opt-in container frame.

size
"sm""md""lg""xs"
default "md"

Border radius matching the size of the inner controls (`controls` variant).

translucent
boolean

Render the container frame with a translucent (frosted) surface.

variant
"controls""stack"
default "controls"

Layout mode. `controls` (default) joins children into one connected unit using their OWN borders — it flattens the corners where they meet and overlaps adjacent borders into a single seam (no group frame, no double border). `stack` renders children as an avatar face pile with overlap and leftmost-on-top cascade.

Plus all standard <div> HTML attributes.

Type

  • Components
  • Blocks