- Accordion
- Alert
- Alert Dialog
- Autocomplete
- Avatar
- Badge
- Breadcrumb
- Button
- Card
- Checkbox
- Checkbox Group
- Collapsible
- Combobox
- Dialog
- EmptyNew
- Field
- Fieldset
- Form
- Frame
- Group
- Input
- Input GroupNew
- KbdNew
- Label
- Menu
- Meter
- Number Field
- Pagination
- Popover
- Preview Card
- Progress
- Radio Group
- Scroll Area
- Select
- Separator
- Sheet
- SkeletonNew
- Slider
- SpinnerNew
- Switch
- Table
- Tabs
- Textarea
- Toast
- Toggle
- Toggle Group
- Toolbar
- Tooltip
Group
A component for visually grouping a series of controls.
import {
ArchiveIcon,
EditIcon,
EllipsisIcon,
FilesIcon,
FilmIcon,
ShareIcon,
TrashIcon,
} from "lucide-react";
import { Button } from "@/components/ui/button";
import { Group, GroupSeparator } from "@/components/ui/group";
import {
Menu,
MenuItem,
MenuPopup,
MenuTrigger,
} from "@/components/ui/menu";
export default function Particle() {
return (
<Group aria-label="File actions">
<Button variant="outline">
<FilesIcon />
Files
</Button>
<GroupSeparator />
<Button variant="outline">
<FilmIcon />
Media
</Button>
<GroupSeparator />
<Menu>
<MenuTrigger
render={<Button aria-label="Menu" size="icon" variant="outline" />}
>
<EllipsisIcon className="size-4" />
</MenuTrigger>
<MenuPopup align="end">
<MenuItem>
<EditIcon />
Edit
</MenuItem>
<MenuItem>
<ArchiveIcon />
Archive
</MenuItem>
<MenuItem>
<ShareIcon />
Share
</MenuItem>
<MenuItem variant="destructive">
<TrashIcon />
Delete
</MenuItem>
</MenuPopup>
</Menu>
</Group>
);
}
Installation
pnpm dlx shadcn@latest add @coss/group
Usage
import { Button } from "@/components/ui/button"
import { Group, GroupSeparator } from "@/components/ui/group"<Group>
<Button>Button</Button>
<GroupSeparator />
<Button>Button</Button>
</Group>Examples
With Input
"use client";
import { CheckIcon, CopyIcon } from "lucide-react";
import { useRef } from "react";
import { useCopyToClipboard } from "@/hooks/use-copy-to-clipboard";
import { Button } from "@/components/ui/button";
import { Group, GroupSeparator } from "@/components/ui/group";
import { Input } from "@/components/ui/input";
import {
Tooltip,
TooltipPopup,
TooltipTrigger,
} from "@/components/ui/tooltip";
export default function Particle() {
const { copyToClipboard, isCopied } = useCopyToClipboard();
const inputRef = useRef<HTMLInputElement>(null);
return (
<Group aria-label="Url input">
<Input
aria-label="Url"
defaultValue="https://coss.com"
ref={inputRef}
type="text"
/>
<GroupSeparator />
<Tooltip>
<TooltipTrigger
render={
<Button
aria-label="Copy"
onClick={() => {
if (inputRef.current) {
copyToClipboard(inputRef.current.value);
}
}}
size="icon"
variant="outline"
/>
}
>
{isCopied ? <CheckIcon /> : <CopyIcon />}
</TooltipTrigger>
<TooltipPopup>
<p>Copy to clipboard</p>
</TooltipPopup>
</Tooltip>
</Group>
);
}
Small Size
import {
ArchiveIcon,
EditIcon,
EllipsisIcon,
FilesIcon,
FilmIcon,
ShareIcon,
TrashIcon,
} from "lucide-react";
import { Button } from "@/components/ui/button";
import { Group, GroupSeparator } from "@/components/ui/group";
import {
Menu,
MenuItem,
MenuPopup,
MenuTrigger,
} from "@/components/ui/menu";
export default function Particle() {
return (
<Group aria-label="File actions">
<Button size="sm" variant="outline">
<FilesIcon />
Files
</Button>
<GroupSeparator />
<Button size="sm" variant="outline">
<FilmIcon />
Media
</Button>
<GroupSeparator />
<Menu>
<MenuTrigger
render={<Button aria-label="Menu" size="icon-sm" variant="outline" />}
>
<EllipsisIcon className="size-4" />
</MenuTrigger>
<MenuPopup align="end">
<MenuItem>
<EditIcon />
Edit
</MenuItem>
<MenuItem>
<ArchiveIcon />
Archive
</MenuItem>
<MenuItem>
<ShareIcon />
Share
</MenuItem>
<MenuItem variant="destructive">
<TrashIcon />
Delete
</MenuItem>
</MenuPopup>
</Menu>
</Group>
);
}
Large Size
import {
ArchiveIcon,
EditIcon,
EllipsisIcon,
FilesIcon,
FilmIcon,
ShareIcon,
TrashIcon,
} from "lucide-react";
import { Button } from "@/components/ui/button";
import { Group, GroupSeparator } from "@/components/ui/group";
import {
Menu,
MenuItem,
MenuPopup,
MenuTrigger,
} from "@/components/ui/menu";
export default function Particle() {
return (
<Group aria-label="File actions">
<Button size="lg" variant="outline">
<FilesIcon />
Files
</Button>
<GroupSeparator />
<Button size="lg" variant="outline">
<FilmIcon />
Media
</Button>
<GroupSeparator />
<Menu>
<MenuTrigger
render={<Button aria-label="Menu" size="icon-lg" variant="outline" />}
>
<EllipsisIcon className="size-4" />
</MenuTrigger>
<MenuPopup align="end">
<MenuItem>
<EditIcon />
Edit
</MenuItem>
<MenuItem>
<ArchiveIcon />
Archive
</MenuItem>
<MenuItem>
<ShareIcon />
Share
</MenuItem>
<MenuItem variant="destructive">
<TrashIcon />
Delete
</MenuItem>
</MenuPopup>
</Menu>
</Group>
);
}
With Disabled Button
import {
ArchiveIcon,
EditIcon,
EllipsisIcon,
FilesIcon,
FilmIcon,
ShareIcon,
TrashIcon,
} from "lucide-react";
import { Button } from "@/components/ui/button";
import { Group, GroupSeparator } from "@/components/ui/group";
import {
Menu,
MenuItem,
MenuPopup,
MenuTrigger,
} from "@/components/ui/menu";
export default function Particle() {
return (
<Group aria-label="File actions">
<Button variant="outline">
<FilesIcon />
Files
</Button>
<GroupSeparator />
<Button disabled variant="outline">
<FilmIcon />
Media
</Button>
<GroupSeparator />
<Menu>
<MenuTrigger
render={<Button aria-label="Menu" size="icon" variant="outline" />}
>
<EllipsisIcon className="size-4" />
</MenuTrigger>
<MenuPopup align="end">
<MenuItem>
<EditIcon />
Edit
</MenuItem>
<MenuItem>
<ArchiveIcon />
Archive
</MenuItem>
<MenuItem>
<ShareIcon />
Share
</MenuItem>
<MenuItem variant="destructive">
<TrashIcon />
Delete
</MenuItem>
</MenuPopup>
</Menu>
</Group>
);
}
With Default Buttons
import {
ArchiveIcon,
EditIcon,
EllipsisIcon,
FilesIcon,
FilmIcon,
ShareIcon,
TrashIcon,
} from "lucide-react";
import { Button } from "@/components/ui/button";
import { Group, GroupSeparator } from "@/components/ui/group";
import {
Menu,
MenuItem,
MenuPopup,
MenuTrigger,
} from "@/components/ui/menu";
export default function Particle() {
return (
<Group aria-label="File actions">
<Button>
<FilesIcon />
Files
</Button>
<GroupSeparator className="bg-primary/72" />
<Button>
<FilmIcon />
Media
</Button>
<GroupSeparator className="bg-primary/72" />
<Menu>
<MenuTrigger render={<Button aria-label="Menu" size="icon" />}>
<EllipsisIcon className="size-4" />
</MenuTrigger>
<MenuPopup align="end">
<MenuItem>
<EditIcon />
Edit
</MenuItem>
<MenuItem>
<ArchiveIcon />
Archive
</MenuItem>
<MenuItem>
<ShareIcon />
Share
</MenuItem>
<MenuItem variant="destructive">
<TrashIcon />
Delete
</MenuItem>
</MenuPopup>
</Menu>
</Group>
);
}
With Start Labeled Text
import { Group, GroupSeparator, GroupText } from "@/components/ui/group";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
export default function Particle() {
return (
<Group aria-label="Domain input">
<GroupText render={<Label aria-label="Domain" htmlFor="domain" />}>
https://
</GroupText>
<GroupSeparator />
<Input
aria-label="Domain"
defaultValue="coss.com"
id="domain"
type="text"
/>
</Group>
);
}
With End Text
import { Group, GroupSeparator, GroupText } from "@/components/ui/group";
import { Input } from "@/components/ui/input";
export default function Particle() {
return (
<Group aria-label="Domain input">
<Input aria-label="Domain" defaultValue="coss" id="domain" type="text" />
<GroupSeparator />
<GroupText>.com</GroupText>
</Group>
);
}
Vertical
import { ZoomInIcon, ZoomOutIcon } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Group, GroupSeparator } from "@/components/ui/group";
export default function Particle() {
return (
<Group aria-label="Zoom controls" orientation="vertical">
<Button aria-label="Zoom in" size="icon" variant="outline">
<ZoomInIcon />
</Button>
<GroupSeparator orientation="horizontal" />
<Button aria-label="Zoom Out" size="icon" variant="outline">
<ZoomOutIcon />
</Button>
</Group>
);
}
Nested Groups
import { ArrowLeftIcon, ArrowRightIcon } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Group, GroupSeparator } from "@/components/ui/group";
export default function Particle() {
return (
<Group aria-label="Pagination">
<Group aria-label="Page numbers">
<Button className="min-w-8" variant="outline">
1
</Button>
<GroupSeparator />
<Button className="min-w-8" variant="outline">
2
</Button>
<GroupSeparator />
<Button className="min-w-8" variant="outline">
3
</Button>
<GroupSeparator />
<Button className="min-w-8" variant="outline">
4
</Button>
<GroupSeparator />
<Button className="min-w-8" variant="outline">
5
</Button>
</Group>
<Group aria-label="Navigation">
<Button aria-label="Previous" size="icon" variant="outline">
<ArrowLeftIcon />
</Button>
<GroupSeparator />
<Button aria-label="Next" size="icon" variant="outline">
<ArrowRightIcon />
</Button>
</Group>
</Group>
);
}
With Popup
import { ChevronDownIcon, GitForkIcon } from "lucide-react";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Group, GroupSeparator } from "@/components/ui/group";
import {
Popover,
PopoverDescription,
PopoverPopup,
PopoverTitle,
PopoverTrigger,
} from "@/components/ui/popover";
export default function Particle() {
return (
<Group aria-label="Repository actions">
<Button variant="outline">
<GitForkIcon />
Fork
<Badge variant="secondary">48</Badge>
</Button>
<GroupSeparator />
<Popover>
<PopoverTrigger
render={
<Button aria-label="Send options" size="icon" variant="outline" />
}
>
<ChevronDownIcon />
</PopoverTrigger>
<PopoverPopup align="end" className="w-64">
<PopoverTitle className="text-base">Existing forks</PopoverTitle>
<PopoverDescription>
You don't have any forks of this repository.
</PopoverDescription>
</PopoverPopup>
</Popover>
</Group>
);
}
With Input Group
import { MicIcon, PaperclipIcon } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Group } from "@/components/ui/group";
import {
InputGroup,
InputGroupAddon,
InputGroupInput,
} from "@/components/ui/input-group";
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@/components/ui/tooltip";
export default function Particle() {
return (
<Group
aria-label="Message composer"
className="[--radius-lg:9999px] [--radius:9999rem]"
>
<Group aria-label="Attachments">
<Button aria-label="Attach file" size="icon" variant="outline">
<PaperclipIcon />
</Button>
</Group>
<Group aria-label="Message input">
<InputGroup>
<InputGroupInput placeholder="Send a message" />
<InputGroupAddon align="inline-end">
<Tooltip>
<TooltipTrigger
render={
<Button
aria-label="Voice Mode"
size="icon-xs"
variant="ghost"
/>
}
>
<MicIcon />
</TooltipTrigger>
<TooltipContent>Voice Mode</TooltipContent>
</Tooltip>
</InputGroupAddon>
</InputGroup>
</Group>
</Group>
);
}
With Menu
import {
ChevronDownIcon,
DownloadIcon,
EditIcon,
ShareIcon,
} from "lucide-react";
import { Button } from "@/components/ui/button";
import { Group, GroupSeparator } from "@/components/ui/group";
import {
Menu,
MenuItem,
MenuPopup,
MenuTrigger,
} from "@/components/ui/menu";
export default function Particle() {
return (
<Group aria-label="Subscription actions">
<Button>Subscribe</Button>
<GroupSeparator className="bg-primary/72" />
<Menu>
<MenuTrigger render={<Button aria-label="Copy options" size="icon" />}>
<ChevronDownIcon className="size-4" />
</MenuTrigger>
<MenuPopup align="end">
<MenuItem>
<ShareIcon />
Share link
</MenuItem>
<MenuItem>
<DownloadIcon />
Download
</MenuItem>
<MenuItem>
<EditIcon />
Duplicate
</MenuItem>
</MenuPopup>
</Menu>
</Group>
);
}
With Select
"use client";
import { ArrowRightIcon } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Group, GroupSeparator } from "@/components/ui/group";
import {
NumberField,
NumberFieldGroup,
NumberFieldInput,
} from "@/components/ui/number-field";
import {
Select,
SelectItem,
SelectPopup,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
interface Currency {
value: string;
label: string;
}
const currencies: Currency[] = [
{
label: "US Dollar",
value: "$",
},
{
label: "Euro",
value: "€",
},
{
label: "British Pound",
value: "£",
},
];
export default function Particle() {
return (
<Group aria-label="Payment amount">
<Group aria-label="Amount input">
<Select
defaultValue={currencies[0]}
itemToStringValue={(currency) => currency.value}
>
<SelectTrigger className="w-fit min-w-none">
<SelectValue>{(currency: Currency) => currency.value}</SelectValue>
</SelectTrigger>
<SelectPopup className="min-w-48">
{currencies.map((curr) => (
<SelectItem key={curr.value} value={curr}>
{curr.value}{" "}
<span className="text-muted-foreground">{curr.label}</span>
</SelectItem>
))}
</SelectPopup>
</Select>
<GroupSeparator />
<NumberField
aria-label="Enter the amount"
defaultValue={10}
render={<NumberFieldGroup />}
>
<NumberFieldInput className="text-left" />
</NumberField>
</Group>
<Group aria-label="Submit">
<Button aria-label="Send" size="icon" variant="outline">
<ArrowRightIcon />
</Button>
</Group>
</Group>
);
}
Accessibility
- The
Groupcomponent has theroleattribute set togroup. - Use
Tabto navigate between the controls in the group. - Use
aria-labeloraria-labelledbyto label the group.
<Group aria-label="Media controls">
<Button variant="outline">Play</Button>
<GroupSeparator />
<Button variant="outline">Pause</Button>
</Group>Group vs ToggleGroup
- Use the
Groupcomponent when you want to group controls that perform an action. - Use the
ToggleGroupcomponent when you want to group controls that toggle a state.
API Reference
Group
The Group component is a container that visually groups a series of related controls together with consistent styling.
| Prop | Type | Default |
|---|---|---|
orientation | "horizontal" | "vertical" | "horizontal" |
<Group>
<Button>Button 1</Button>
<GroupSeparator />
<Button>Button 2</Button>
</Group>Nest multiple groups to create complex layouts with spacing. See the nested groups example for more details.
<Group>
<Group>
<Button>1</Button>
<GroupSeparator />
<Button>2</Button>
</Group>
<Group>
<Button>Previous</Button>
<GroupSeparator />
<Button>Next</Button>
</Group>
</Group>GroupSeparator
The GroupSeparator component visually divides controls within a group.
| Prop | Type | Default |
|---|---|---|
orientation | "horizontal" | "vertical" | "vertical" |
<Group>
<Button>Button 1</Button>
<GroupSeparator />
<Button>Button 2</Button>
</Group>Note: Unlike shadcn's ButtonGroup, GroupSeparator is required between all controls, including outline buttons. This ensures consistent visual hierarchy and focus states.
GroupText
Use this component to display text within a group, such as labels or prefixes.
| Prop | Type | Default |
|---|---|---|
render | React.ReactNode | null | null |
<Group>
<GroupText>https://</GroupText>
<GroupSeparator />
<Input placeholder="example.com" />
</Group>Use the render prop to render a custom component as the text, for example a label.
<Group>
<GroupText render={<Label htmlFor="domain" aria-label="Domain" />}>
https://
</GroupText>
<GroupSeparator />
<Input id="domain" placeholder="example.com" />
</Group>Comparing with shadcn
If you're already familiar with shadcn's ButtonGroup, this guide highlights the small differences and similarities so you can get started with coss ui quickly.
Quick Checklist
- Prefer
Group*component names;ButtonGroup*remain for compatibility GroupSeparatoris always required between controls, including outline buttons (unlike shadcn where separators are optional for outline buttons). This ensures consistent focus state handling and better accessibility- If you used
asChildonButtonGroupText, switch to therenderprop for custom components
On This Page