icon

Morainev0.5.0

ButtonGroup
elementsbutton-group

ButtonGroup

Cohesive group of related buttons with shared size, variant, and orientation.

Import#

import { Button, ButtonGroup } from 'moraine'

Usage#

ButtonGroup joins the edges of direct button children and provides shared size and variant defaults. A child Button can override either default when needed.

Add an accessible label when the surrounding content does not already name the group.

Set separator to render a decorative separator between adjacent controls. Customize its root with the separator slot in classes or styles.

<ButtonGroup separator classes={{ separator: 'text-primary' }}>
<Button>Export</Button>
<Button>Share</Button>
</ButtonGroup>

Slot Structure#

root
├── Button (x n)
└── separator (between adjacent children, optional)

Examples#

Basic#

function Basic() {
return (
<ButtonGroup variant="outline" aria-label="Document history">
<Button leading="i-lucide:undo-2">Undo</Button>
<Button leading="i-lucide:redo-2">Redo</Button>
</ButtonGroup>
)
}

Variants#

function Variants() {
const VARIANTS: ButtonGroupVariant[] = ['default', 'secondary', 'outline', 'ghost', 'destructive']
return (
<div class="flex flex-wrap gap-3">
<For each={VARIANTS}>
{(variant) => (
<ButtonGroup variant={variant} aria-label={`${variant} actions`}>
<Button>Previous</Button>
<Button>Next</Button>
</ButtonGroup>
)}
</For>
</div>
)
}

Sizes#

function Sizes() {
const SIZES: ButtonGroupSize[] = ['xs', 'sm', 'md', 'lg', 'xl']
return (
<div class="flex flex-wrap gap-3 items-end">
<For each={SIZES}>
{(size) => (
<ButtonGroup size={size} variant="outline" aria-label={`${size} pagination`}>
<Button>1</Button>
<Button>2</Button>
<Button>3</Button>
</ButtonGroup>
)}
</For>
</div>
)
}

Orientations#

Horizontal and vertical groups using compact quantity controls.

function Vertical() {
return (
<div class="flex flex-wrap gap-6 items-start">
<ButtonGroup variant="outline" aria-label="Horizontal quantity controls">
<Button size="icon-md" leading="i-lucide:minus" aria-label="Decrease quantity" />
<Button size="icon-md" leading="i-lucide:plus" aria-label="Increase quantity" />
</ButtonGroup>
<ButtonGroup orientation="vertical" variant="outline" aria-label="Vertical quantity controls">
<Button size="icon-md" leading="i-lucide:plus" aria-label="Increase quantity" />
<Button size="icon-md" leading="i-lucide:minus" aria-label="Decrease quantity" />
</ButtonGroup>
</div>
)
}

Separators#

Toggle separators between adjacent controls with a switch.

function Separator() {
const [showSeparator, setShowSeparator] = createSignal(true)
return (
<div class="flex flex-col gap-4 items-start">
<Switch label="Show separators" checked={showSeparator()} onChange={setShowSeparator} />
<ButtonGroup separator={showSeparator()} aria-label="Document actions">
<Button leading="i-lucide:download">Export</Button>
<Button leading="i-lucide:share-2">Share</Button>
<Button leading="i-lucide:archive">Archive</Button>
</ButtonGroup>
</div>
)
}

Popover#

Compose a primary button with a Popover trigger inside the same group.

function ButtonPopover() {
return (
<ButtonGroup separator aria-label="Document actions">
<Button leading="i-lucide:save">Save document</Button>
<Popover
content={
<div class="space-y-1">
<p class="text-sm font-medium">Save options</p>
<p class="text-xs text-muted-foreground">Choose where to save this document.</p>
</div>
}
>
<Button size="icon-md" aria-label="Open save options">
<Icon name="i-lucide:chevron-down" />
</Button>
</Popover>
</ButtonGroup>
)
}

A report export action composed from a button trigger and a dropdown menu.

function DropdownAction() {
const [exportedFormat, setExportedFormat] = createSignal<string>()
const exportItems: DropdownMenuT.Item[] = [
{
type: 'group',
label: 'Export report',
children: [
{
label: 'PDF document',
description: 'Best for sharing and printing',
icon: 'i-lucide:file-text',
onSelect: () => setExportedFormat('PDF'),
},
{
label: 'CSV spreadsheet',
description: 'Best for analysis and imports',
icon: 'i-lucide:table-2',
onSelect: () => setExportedFormat('CSV'),
},
{
label: 'JSON data',
description: 'Best for integrations',
icon: 'i-lucide:braces',
onSelect: () => setExportedFormat('JSON'),
},
],
},
]
return (
<div class="flex flex-col gap-3 items-start">
<ButtonGroup separator>
<Button leading="i-lucide:download">Export report</Button>
<DropdownMenu items={exportItems}>
<Button size="icon-md">
<Icon name="i-lucide:chevron-down" />
</Button>
</DropdownMenu>
</ButtonGroup>
<p class="text-sm text-muted-foreground min-h-5" role="status" aria-live="polite">
{exportedFormat() ? `Report exported as ${exportedFormat()}.` : 'Choose an export format.'}
</p>
</div>
)
}

API Reference#

Attributes#

Slotroot4 attributes
Container that joins the edges of its direct button children.

Data Attributes

3
Data AttributeDescription
data-orientation
Stores the rendered orientation.
data-size
Stores the resolved size variant.
data-variant
Stores the resolved visual variant.

ARIA Attributes

1
ARIA AttributeDescription
role
Defines the semantic role exposed to assistive technology.

Props#

PropTypeDefaultDescription
childrenJSX.Element
Buttons or compatible controls rendered as a cohesive group.
classClassValue
Class applied to the component root or trigger element.
classesButtonGroupT.Classes | undefined
idstring | undefined
Optional identifier for the group root.
orientation"horizontal" | "vertical" | undefined
refJSX.HTMLElementTags["div"] extends { ref?: infer Ref; } ? Ref : never | undefined
roleJSX.AriaAttributes["role"] | undefined
ARIA role for the group root.
separatorboolean | undefined
Whether to render a decorative separator between adjacent controls.
size"xs" | "sm" | "md" | "lg" | "xl" | "icon-xs" | "icon-sm" | "icon-md" | "icon-lg" | "icon-xl" | undefined
styleJSX.CSSProperties | undefined
stylesButtonGroupT.Styles | undefined
variant"link" | "default" | "destructive" | "outline" | "secondary" | "ghost" | undefined