Button grouping container with orientation support
import { Button } from '@/components/ui/button'
import { ButtonGroup } from '@/components/ui/button-group'
...import { Button } from '@/components/ui/button'
import { ButtonGroup } from '@/components/ui/button-group'
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuRadio,
DropdownMenuRadioItem,
DropdownMenuSeparator,
DropdownMenuSub,
DropdownMenuSubContent,
DropdownMenuSubTrigger,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu'
function MoreIcon() {
return <svg viewBox="0 0 24 24" aria-hidden="true"><circle cx="5" cy="12" r="1" /><circle cx="12" cy="12" r="1" /><circle cx="19" cy="12" r="1" /></svg>
}
export default function ButtonGroupDemoExample() {
let label = $state('personal')
return (
<div class="flex flex-wrap gap-2">
<ButtonGroup><Button variant="outline" size="icon" aria-label="Go back">←</Button></ButtonGroup>
<ButtonGroup><Button variant="outline">Archive</Button><Button variant="outline">Report</Button></ButtonGroup>
<ButtonGroup>
<Button variant="outline">Snooze</Button>
<DropdownMenu>
<DropdownMenuTrigger asChild><Button variant="outline" size="icon" aria-label="More options"><MoreIcon /></Button></DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem>Mark as Read</DropdownMenuItem>
<DropdownMenuItem>Archive</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem>Snooze</DropdownMenuItem>
<DropdownMenuItem>Add to Calendar</DropdownMenuItem>
<DropdownMenuItem>Add to List</DropdownMenuItem>
<DropdownMenuSub>
<DropdownMenuSubTrigger>Label As...</DropdownMenuSubTrigger>
<DropdownMenuSubContent>
<DropdownMenuRadio value={label} onValueChange={value => { label = String(value) }}>
<DropdownMenuRadioItem value="personal">Personal</DropdownMenuRadioItem>
<DropdownMenuRadioItem value="work">Work</DropdownMenuRadioItem>
<DropdownMenuRadioItem value="other">Other</DropdownMenuRadioItem>
</DropdownMenuRadio>
</DropdownMenuSubContent>
</DropdownMenuSub>
<DropdownMenuSeparator />
<DropdownMenuItem>Trash</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</ButtonGroup>
</div>
)
}
Installation
npx @fictjs/shadcn@latest add button-groupInstall the runtime dependencies:
npm install class-variance-authorityCopy the Fict shadcn component into your project.
Source
components/ui/button-group.tsx
src/components/ui/button-group.tsx
import { cva, type VariantProps } from 'class-variance-authority'
import { cn } from '@/lib/cn'
const buttonGroupVariants = cva('inline-flex', {
variants: {
orientation: {
horizontal: 'flex-row',
vertical: 'flex-col',
},
attached: {
true: '[&>*]:rounded-none [&>*:first-child]:rounded-l-md [&>*:last-child]:rounded-r-md [&>*:not(:first-child)]:-ml-px',
false: 'gap-2',
},
},
defaultVariants: {
orientation: 'horizontal',
attached: true,
},
})
type ButtonGroupProps = JSX.IntrinsicElements['div'] & VariantProps<typeof buttonGroupVariants>
export function ButtonGroup(props: ButtonGroupProps) {
const { class: className, orientation, attached, ...rest } = props
return <div role='group' class={cn(buttonGroupVariants({ orientation, attached }), className)} {...rest} />
}
export function ButtonGroupSeparator(props: JSX.IntrinsicElements['div']) {
const { class: className, ...rest } = props
return <div role='separator' class={cn('self-stretch bg-border data-[orientation=vertical]:w-px data-[orientation=horizontal]:h-px', className)} {...rest} />
}
Usage
Examples
Orientation
import { Button } from '@/components/ui/button'
import { ButtonGroup } from '@/components/ui/button-group'
...import { Button } from '@/components/ui/button'
import { ButtonGroup } from '@/components/ui/button-group'
export default function ButtonGroupOrientationExample() {
return (
<ButtonGroup orientation="vertical" aria-label="Media controls" class="h-fit">
<Button variant="outline" size="icon">
<span aria-hidden="true">•</span>
</Button>
<Button variant="outline" size="icon">
<span aria-hidden="true">•</span>
</Button>
</ButtonGroup>
)
}
Size
import { Button } from '@/components/ui/button'
import { ButtonGroup } from '@/components/ui/button-group'
...import { Button } from '@/components/ui/button'
import { ButtonGroup } from '@/components/ui/button-group'
export default function ButtonGroupSizeExample() {
return (
<div class="flex flex-col items-start gap-8">
<ButtonGroup>
<Button variant="outline" size="sm">
Small
</Button>
<Button variant="outline" size="sm">
Button
</Button>
<Button variant="outline" size="sm">
Group
</Button>
<Button variant="outline" size="icon-sm">
<span aria-hidden="true">•</span>
</Button>
</ButtonGroup>
<ButtonGroup>
<Button variant="outline">Default</Button>
<Button variant="outline">Button</Button>
<Button variant="outline">Group</Button>
<Button variant="outline" size="icon">
<span aria-hidden="true">•</span>
</Button>
</ButtonGroup>
<ButtonGroup>
<Button variant="outline" size="lg">
Large
</Button>
<Button variant="outline" size="lg">
Button
</Button>
<Button variant="outline" size="lg">
Group
</Button>
<Button variant="outline" size="icon-lg">
<span aria-hidden="true">•</span>
</Button>
</ButtonGroup>
</div>
)
}
Nested
import { Button } from '@/components/ui/button'
import { ButtonGroup } from '@/components/ui/button-group'
...import { Button } from '@/components/ui/button'
import { ButtonGroup } from '@/components/ui/button-group'
import { Input } from '@/components/ui/input'
import { InputGroup, InputGroupAddon, InputGroupInput } from '@/components/ui/input-group'
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
export default function ButtonGroupNestedExample() {
return (
<ButtonGroup>
<ButtonGroup>
<Button variant="outline" size="icon">
<span aria-hidden="true">•</span>
</Button>
</ButtonGroup>
<ButtonGroup>
<InputGroup>
<InputGroupInput placeholder="Send a message..." />
<Tooltip>
<TooltipTrigger asChild>
<InputGroupAddon align="inline-end">
<span aria-hidden="true">•</span>
</InputGroupAddon>
</TooltipTrigger>
<TooltipContent>Voice Mode</TooltipContent>
</Tooltip>
</InputGroup>
</ButtonGroup>
</ButtonGroup>
)
}
Separator
import { Button } from '@/components/ui/button'
import { ButtonGroup, ButtonGroupSeparator } from '@/components/ui/button-group'
...import { Button } from '@/components/ui/button'
import { ButtonGroup, ButtonGroupSeparator } from '@/components/ui/button-group'
export default function ButtonGroupSeparatorDemoExample() {
return (
<ButtonGroup>
<Button variant="secondary" size="sm">
Copy
</Button>
<ButtonGroupSeparator />
<Button variant="secondary" size="sm">
Paste
</Button>
</ButtonGroup>
)
}
Split
import { Button } from '@/components/ui/button'
import { ButtonGroup, ButtonGroupSeparator } from '@/components/ui/button-group'
...import { Button } from '@/components/ui/button'
import { ButtonGroup, ButtonGroupSeparator } from '@/components/ui/button-group'
export default function ButtonGroupSplitExample() {
return (
<ButtonGroup>
<Button variant="secondary">Button</Button>
<ButtonGroupSeparator />
<Button size="icon" variant="secondary">
<span aria-hidden="true">•</span>
</Button>
</ButtonGroup>
)
}
Input
import { Button } from '@/components/ui/button'
import { ButtonGroup } from '@/components/ui/button-group'
...import { Button } from '@/components/ui/button'
import { ButtonGroup } from '@/components/ui/button-group'
import { Input } from '@/components/ui/input'
export default function ButtonGroupInputExample() {
return (
<ButtonGroup>
<Input placeholder="Search..." />
<Button variant="outline" aria-label="Search">
<span aria-hidden="true">•</span>
</Button>
</ButtonGroup>
)
}
Input Group
import { Button } from '@/components/ui/button'
import { ButtonGroup } from '@/components/ui/button-group'
...import { Button } from '@/components/ui/button'
import { ButtonGroup } from '@/components/ui/button-group'
import {
InputGroup,
InputGroupAddon,
InputGroupButton,
InputGroupInput,
} from '@/components/ui/input-group'
export default function ButtonGroupInputGroupExample() {
let voiceEnabled = $state(false)
return (
<ButtonGroup>
<Button variant="outline" size="icon" aria-label="Add">
+
</Button>
<InputGroup>
<InputGroupInput placeholder="Send a message..." />
<InputGroupAddon align="inline-end">
<InputGroupButton
aria-pressed={voiceEnabled}
onClick={() => {
voiceEnabled = !voiceEnabled
}}
aria-label="Voice input"
>
{voiceEnabled ? '●' : '○'}
</InputGroupButton>
</InputGroupAddon>
</InputGroup>
</ButtonGroup>
)
}
Dropdown
import { Button } from '@/components/ui/button'
import { ButtonGroup } from '@/components/ui/button-group'
...import { Button } from '@/components/ui/button'
import { ButtonGroup } from '@/components/ui/button-group'
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu'
export default function ButtonGroupDropdownExample() {
return (
<ButtonGroup>
<Button variant="outline">Follow</Button>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" class="pl-2!">
<span aria-hidden="true">•</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" class="w-44">
<DropdownMenuItem>
<span aria-hidden="true">•</span>
Mute Conversation
</DropdownMenuItem>
<DropdownMenuItem>
<span aria-hidden="true">•</span>
Mark as Read
</DropdownMenuItem>
<DropdownMenuItem>
<span aria-hidden="true">•</span>
Report Conversation
</DropdownMenuItem>
<DropdownMenuItem>
<span aria-hidden="true">•</span>
Block User
</DropdownMenuItem>
<DropdownMenuItem>
<span aria-hidden="true">•</span>
Share Conversation
</DropdownMenuItem>
<DropdownMenuItem>
<span aria-hidden="true">•</span>
Copy Conversation
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem variant="destructive">
<span aria-hidden="true">•</span>
Delete Conversation
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</ButtonGroup>
)
}
Select
import { Button } from '@/components/ui/button'
import { ButtonGroup } from '@/components/ui/button-group'
...import { Button } from '@/components/ui/button'
import { ButtonGroup } from '@/components/ui/button-group'
import { Input } from '@/components/ui/input'
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select'
export default function ButtonGroupSelectExample() {
let currency = $state('$')
return (
<ButtonGroup>
<Select
value={() => currency}
onValueChange={value => {
currency = value
}}
>
<SelectTrigger class="w-20">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="$">$</SelectItem>
<SelectItem value="€">€</SelectItem>
<SelectItem value="£">£</SelectItem>
</SelectContent>
</Select>
<Input aria-label="Amount" placeholder="0.00" />
<Button variant="outline" size="icon" aria-label="Continue">
→
</Button>
</ButtonGroup>
)
}
Popover
import { Button } from '@/components/ui/button'
import { ButtonGroup } from '@/components/ui/button-group'
...import { Button } from '@/components/ui/button'
import { ButtonGroup } from '@/components/ui/button-group'
import { Field, FieldDescription, FieldLabel } from '@/components/ui/field'
import {
Popover,
PopoverContent,
PopoverDescription,
PopoverHeader,
PopoverTitle,
PopoverTrigger,
} from '@/components/ui/popover'
import { Textarea } from '@/components/ui/textarea'
export default function ButtonGroupPopoverExample() {
return (
<ButtonGroup>
<Button variant="outline">
<span aria-hidden="true">•</span> Copilot
</Button>
<Popover>
<PopoverTrigger asChild>
<Button variant="outline" size="icon" aria-label="Open Popover">
<span aria-hidden="true">•</span>
</Button>
</PopoverTrigger>
<PopoverContent align="end" class="rounded-xl text-sm">
<PopoverHeader>
<PopoverTitle>Start a new task with Copilot</PopoverTitle>
<PopoverDescription>Describe your task in natural language.</PopoverDescription>
</PopoverHeader>
<Field>
<FieldLabel for="task" class="sr-only">
Task Description
</FieldLabel>
<Textarea id="task" placeholder="I need to..." class="resize-none" />
<FieldDescription>Copilot will open a pull request for review.</FieldDescription>
</Field>
</PopoverContent>
</Popover>
</ButtonGroup>
)
}
Rtl
import { Button } from '@/components/ui/button'
import { ButtonGroup } from '@/components/ui/button-group'
...import { Button } from '@/components/ui/button'
import { ButtonGroup } from '@/components/ui/button-group'
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger } from '@/components/ui/dropdown-menu'
const translations = {
ar: { dir: 'rtl', archive: 'أرشفة', report: 'تقرير', snooze: 'تأجيل', read: 'وضع علامة كمقروء', trash: 'سلة المهملات' },
he: { dir: 'rtl', archive: 'ארכיון', report: 'דוח', snooze: 'דחה', read: 'סמן כנקרא', trash: 'פח' },
en: { dir: 'ltr', archive: 'Archive', report: 'Report', snooze: 'Snooze', read: 'Mark as Read', trash: 'Trash' },
} as const
export default function ButtonGroupRtlExample() {
let language = $state<keyof typeof translations>('ar')
const t = translations[language]
return (
<div class="grid gap-4"><select aria-label="Preview language" value={language} onChange={event => language = event.currentTarget.value as keyof typeof translations}><option value="ar">Arabic (العربية)</option><option value="he">Hebrew (עברית)</option><option value="en">English</option></select><div class="flex gap-3" dir={t.dir}><ButtonGroup><Button variant="outline" size="icon" aria-label="Go Back">←</Button></ButtonGroup><ButtonGroup><Button variant="outline">{t.archive}</Button><Button variant="outline">{t.report}</Button></ButtonGroup><ButtonGroup><Button variant="outline">{t.snooze}</Button><DropdownMenu dir={t.dir}><DropdownMenuTrigger asChild><Button variant="outline" size="icon" aria-label="More Options">•••</Button></DropdownMenuTrigger><DropdownMenuContent align="end"><DropdownMenuItem>{t.read}</DropdownMenuItem><DropdownMenuItem>{t.archive}</DropdownMenuItem><DropdownMenuSeparator /><DropdownMenuItem variant="destructive">{t.trash}</DropdownMenuItem></DropdownMenuContent></DropdownMenu></ButtonGroup></div></div>
)
}