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".
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.
---
// GENERATED from packages/demos/group/basic.demo.tsx — edit the source, not this file.
import Group from "@pindoba/astro-group";
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="astro-demo-basic-selected-fw-env-dev"
appearance="button"
checked
value="dev"
fullWidth
name="astro-demo-basic-selected-fw-env"
>
Dev
</Radio>
<Radio
id="astro-demo-basic-selected-fw-env-staging"
appearance="button"
value="staging"
fullWidth
name="astro-demo-basic-selected-fw-env"
>
Staging
</Radio>
<Radio
id="astro-demo-basic-selected-fw-env-prod"
appearance="button"
value="prod"
fullWidth
name="astro-demo-basic-selected-fw-env"
>
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 tone buttons</h3>
<Group variant="controls">
<Button tone="success">Save</Button>
<Button tone="warning" emphasis="secondary">
Draft
</Button>
<Button tone="danger" emphasis="secondary">
Delete
</Button>
</Group>
</div>
<div>
<h3>Tab horizontal</h3>
<Group variant="controls">
<Radio
id="astro-demo-basic-selected-tab-all"
appearance="tab"
checked
value="all"
name="astro-demo-basic-selected-tab"
>
All
</Radio>
<Radio
id="astro-demo-basic-selected-tab-active"
appearance="tab"
value="active"
name="astro-demo-basic-selected-tab"
>
Active
</Radio>
<Radio
id="astro-demo-basic-selected-tab-archived"
appearance="tab"
value="archived"
name="astro-demo-basic-selected-tab"
>
Archived
</Radio>
</Group>
</div>
<div>
<h3>Tab vertical</h3>
<Group variant="controls" orientation="vertical">
<Radio
id="astro-demo-basic-selected-nav-dashboard"
appearance="tab"
orientation="vertical"
checked
value="dashboard"
name="astro-demo-basic-selected-nav"
>
Dashboard
</Radio>
<Radio
id="astro-demo-basic-selected-nav-settings"
appearance="tab"
orientation="vertical"
value="settings"
name="astro-demo-basic-selected-nav"
>
Settings
</Radio>
<Radio
id="astro-demo-basic-selected-nav-profile"
appearance="tab"
orientation="vertical"
value="profile"
name="astro-demo-basic-selected-nav"
>
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="astro-demo-basic-selected-view-list"
appearance="button"
size="md"
checked
value="list"
name="astro-demo-basic-selected-view"
>
List
</Radio>
<Radio
id="astro-demo-basic-selected-view-grid"
appearance="button"
size="md"
value="grid"
name="astro-demo-basic-selected-view"
>
Grid
</Radio>
<Radio
id="astro-demo-basic-selected-view-table"
appearance="button"
size="md"
value="table"
name="astro-demo-basic-selected-view"
>
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="astro-demo-basic-selected-view-list"
appearance="button"
size="md"
checked
value="list"
name="astro-demo-basic-selected-view"
>
List
</Radio>
<Radio
id="astro-demo-basic-selected-view-grid"
appearance="button"
size="md"
value="grid"
name="astro-demo-basic-selected-view"
>
Grid
</Radio>
<Radio
id="astro-demo-basic-selected-view-table"
appearance="button"
size="md"
value="table"
name="astro-demo-basic-selected-view"
>
Table
</Radio>
</Group>
</div>
</div>With variant="controls", apply a semantic color to the merged frame and pass
matching tone to the inner components.
---
// GENERATED from packages/demos/group/tone.demo.tsx — edit the source, not this file.
import Group from "@pindoba/astro-group";
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" tone="neutral">
<Input placeholder="Neutral" />
<Button>Go</Button>
</Group>
<Group variant="controls" tone="primary">
<Input tone="primary" placeholder="Primary" />
<Button>Go</Button>
</Group>
<Group variant="controls" tone="success">
<Input tone="success" placeholder="Success" />
<Button tone="success">Go</Button>
</Group>
<Group variant="controls" tone="danger">
<Input tone="danger" placeholder="Danger" />
<Button tone="danger">Go</Button>
</Group>
<Group variant="controls" tone="warning">
<Input tone="warning" placeholder="Warning" />
<Button tone="warning">Go</Button>
</Group>
</div>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.
---
// GENERATED from packages/demos/group/size.demo.tsx — edit the source, not this file.
import Group from "@pindoba/astro-group";
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>controls also merges badges — and a trailing button — into a single chip. Mix
emphasis (primary / secondary / tertiary) and tone 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.
---
// GENERATED from packages/demos/group/badges.demo.tsx — edit the source, not this file.
import Badge from "@pindoba/astro-badge";
import Group from "@pindoba/astro-group";
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" })}>
<div
class={stack({
gap: "md",
direction: "row",
align: "center",
flexWrap: "wrap",
})}
>
<Group variant="controls">
<Badge tone="primary" emphasis="primary">
React
</Badge>
<Badge tone="primary" emphasis="secondary">
18 issues
</Badge>
</Group>
<Group variant="controls">
<Badge tone="success" emphasis="secondary">
v2.1
</Badge>
<Badge tone="success" emphasis="primary">
Released
</Badge>
</Group>
<Group variant="controls">
<Badge tone="neutral" emphasis="tertiary">
Filter
</Badge>
<Badge tone="primary" emphasis="secondary">
12 results
</Badge>
</Group>
<Group variant="controls">
<Badge tone="warning" emphasis="primary">
Pending
</Badge>
<Badge tone="warning" emphasis="tertiary">
3 in queue
</Badge>
</Group>
</div>
<div
class={stack({
gap: "md",
direction: "row",
align: "center",
flexWrap: "wrap",
})}
>
<Group variant="controls">
<Badge tone="primary" emphasis="primary">
PR
</Badge>
<Badge tone="primary" emphasis="secondary">
#247
</Badge>
<Badge tone="success" emphasis="tertiary">
merged
</Badge>
</Group>
<Group variant="controls">
<Badge tone="danger" emphasis="primary">
CI
</Badge>
<Badge tone="danger" emphasis="secondary">
failed
</Badge>
<Badge tone="neutral" emphasis="tertiary">
retry in 30s
</Badge>
</Group>
</div>
<div
class={stack({
gap: "md",
direction: "row",
align: "center",
flexWrap: "wrap",
})}
>
<Group variant="controls" size="md">
<Badge tone="primary" emphasis="primary">
Topic
</Badge>
<Badge tone="primary" emphasis="secondary">
design-systems
</Badge>
<Button aria-label="Dismiss" size="sm" shape="square" emphasis="ghost">
<X />
</Button>
</Group>
<Group variant="controls" size="sm">
<Badge size="sm" tone="neutral" emphasis="tertiary">
Author
</Badge>
<Badge size="sm" tone="primary" emphasis="secondary">
@ed
</Badge>
<Button
aria-label="Dismiss"
size="xs"
shape="square"
emphasis="secondary"
>
<X />
</Button>
</Group>
<Group variant="controls" size="md">
<Badge tone="warning" emphasis="primary">
Draft
</Badge>
<Badge tone="neutral" emphasis="tertiary">
12 files
</Badge>
<Button aria-label="Dismiss" size="sm" shape="square" emphasis="ghost">
<X />
</Button>
</Group>
</div>
</div>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.
---
// GENERATED from packages/demos/group/kbd.demo.tsx — edit the source, not this file.
import Kbd from "@pindoba/astro-kbd";
import Group from "@pindoba/astro-group";
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>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.
---
// GENERATED from packages/demos/group/custom.demo.tsx — edit the source, not this file.
import Group from "@pindoba/astro-group";
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>