ButtonGroup
Group related buttons into a connected control with shared size, variant, disabled state, and separators.
Import
vue
<script setup lang="ts">
import { Button, ButtonGroup, ButtonGroupSeparator } from '@heroui-vue/vue'
</script>Usage
Basic
<template>
<div class="demo-button-group-stack">
<ButtonGroup>
<Button>Merge pull request</Button>
<Button is-icon-only aria-label="More options">
<ButtonGroupSeparator />
<DemoIcon name="chevron-down" />
</Button>
</ButtonGroup>
<div class="demo-button-group-row">
<ButtonGroup variant="tertiary">
<Button>
<DemoIcon name="fork" />
Fork
</Button>
<Button is-icon-only aria-label="Fork options">
<ButtonGroupSeparator />
<DemoIcon name="chevron-down" />
</Button>
</ButtonGroup>
<ButtonGroup variant="tertiary">
<Button is-icon-only aria-label="QR code">
<DemoIcon name="qr" />
</Button>
<Button>
<ButtonGroupSeparator />
Scan to pay
</Button>
</ButtonGroup>
<ButtonGroup variant="tertiary">
<Button>
<DemoIcon name="thumb-up" />
2.4K
</Button>
<Button is-icon-only aria-label="Dislike">
<ButtonGroupSeparator />
<DemoIcon name="thumb-down" />
</Button>
</ButtonGroup>
<ButtonGroup variant="tertiary">
<Button>
<DemoIcon name="star" />
Star
</Button>
<Button>
<ButtonGroupSeparator />
104
</Button>
</ButtonGroup>
</div>
<ButtonGroup variant="tertiary">
<Button>
<DemoIcon name="chevron-left" />
Previous
</Button>
<Button>
<ButtonGroupSeparator />
Next
<DemoIcon name="chevron-right" />
</Button>
</ButtonGroup>
</div>
</template>
<script setup lang="ts">
import { defineComponent, h } from 'vue'
import { Button, ButtonGroup, ButtonGroupSeparator } from '@heroui-vue/vue'
const iconPaths: Record<string, string[]> = {
'chevron-down': ['M6 9l6 6 6-6'],
'chevron-left': ['M15 18l-6-6 6-6'],
'chevron-right': ['M9 18l6-6-6-6'],
fork: ['M7 5a2 2 0 1 0 0 4 2 2 0 0 0 0-4Z', 'M17 5a2 2 0 1 0 0 4 2 2 0 0 0 0-4Z', 'M7 9v2a4 4 0 0 0 4 4h2a4 4 0 0 1 4 4v0', 'M17 9v10'],
qr: ['M4 4h6v6H4z', 'M14 4h6v6h-6z', 'M4 14h6v6H4z', 'M14 14h2v2h-2z', 'M18 14h2v6h-4v-2h2z'],
'thumb-up': ['M7 10v10', 'M11 10l1-5a2 2 0 0 1 3 1.8V10h4a2 2 0 0 1 2 2l-1 6a2 2 0 0 1-2 2h-7a4 4 0 0 1-4-4v-2a4 4 0 0 1 4-4Z'],
'thumb-down': ['M17 14V4', 'M13 14l-1 5a2 2 0 0 1-3-1.8V14H5a2 2 0 0 1-2-2l1-6a2 2 0 0 1 2-2h7a4 4 0 0 1 4 4v2a4 4 0 0 1-4 4Z'],
star: ['M12 3l2.7 5.5 6.1.9-4.4 4.3 1 6.1L12 17l-5.4 2.8 1-6.1-4.4-4.3 6.1-.9z'],
}
const DemoIcon = defineComponent({
props: {
name: {
type: String,
required: true,
},
},
setup(props) {
return () =>
h(
'svg',
{
viewBox: '0 0 24 24',
fill: 'none',
stroke: 'currentColor',
'stroke-width': '2',
'stroke-linecap': 'round',
'stroke-linejoin': 'round',
'aria-hidden': 'true',
},
iconPaths[props.name]?.map((d) => h('path', { d })) ?? [],
)
},
})
</script>
<style lang="less">
.demo-button-group-stack {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 1.5rem;
text-align: left;
}
.demo-button-group-row {
display: flex;
flex-wrap: wrap;
align-items: flex-start;
gap: 1rem;
text-align: left;
}
</style>Anatomy
vue
<template>
<ButtonGroup>
<Button>First</Button>
<Button>
<ButtonGroupSeparator />
Second
</Button>
<Button>
<ButtonGroupSeparator />
Third
</Button>
</ButtonGroup>
</template>ButtonGroup passes size, variant, isDisabled, and fullWidth to direct child buttons through Vue provide/inject. Add ButtonGroupSeparator inside each button after the first one when you want dividers.
Variants
Primary
Secondary
Tertiary
Outline
Ghost
Danger
<template>
<div class="demo-button-group-stack">
<div
v-for="variant in variants"
:key="variant"
class="demo-button-group-field"
>
<p class="demo-button-group-label">
{{ labels[variant] }}
</p>
<ButtonGroup :variant="variant">
<Button>First</Button>
<Button>
<ButtonGroupSeparator />
Second
</Button>
<Button>
<ButtonGroupSeparator />
Third
</Button>
</ButtonGroup>
</div>
</div>
</template>
<script setup lang="ts">
import { Button, ButtonGroup, ButtonGroupSeparator } from '@heroui-vue/vue'
type Variant = 'primary' | 'secondary' | 'tertiary' | 'outline' | 'ghost' | 'danger'
const variants: Variant[] = ['primary', 'secondary', 'tertiary', 'outline', 'ghost', 'danger']
const labels: Record<Variant, string> = {
primary: 'Primary',
secondary: 'Secondary',
tertiary: 'Tertiary',
outline: 'Outline',
ghost: 'Ghost',
danger: 'Danger',
}
</script>
<style lang="less">
.demo-button-group-stack {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 1.5rem;
text-align: left;
}
.demo-button-group-field {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 0.5rem;
text-align: left;
}
.demo-button-group-label {
margin: 0;
color: var(--color-muted-foreground);
font-size: 0.875rem;
line-height: 1.25rem;
text-align: left;
}
</style>Sizes
Small
Medium (default)
Large
<template>
<div class="demo-button-group-stack">
<div
v-for="size in sizes"
:key="size.value"
class="demo-button-group-field"
>
<p class="demo-button-group-label">
{{ size.label }}
</p>
<ButtonGroup
:size="size.value"
variant="secondary"
>
<Button>First</Button>
<Button>
<ButtonGroupSeparator />
Second
</Button>
<Button>
<ButtonGroupSeparator />
Third
</Button>
</ButtonGroup>
</div>
</div>
</template>
<script setup lang="ts">
import { Button, ButtonGroup, ButtonGroupSeparator } from '@heroui-vue/vue'
const sizes = [
{ label: 'Small', value: 'sm' },
{ label: 'Medium (default)', value: 'md' },
{ label: 'Large', value: 'lg' },
] as const
</script>
<style lang="less">
.demo-button-group-stack {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 1.5rem;
text-align: left;
}
.demo-button-group-field {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 0.5rem;
text-align: left;
}
.demo-button-group-label {
margin: 0;
color: var(--color-muted-foreground);
font-size: 0.875rem;
line-height: 1.25rem;
text-align: left;
}
</style>Orientation
Horizontal
Vertical
<template>
<div class="demo-button-group-row">
<div class="demo-button-group-field">
<p class="demo-button-group-label">
Horizontal
</p>
<ButtonGroup
orientation="horizontal"
variant="tertiary"
>
<Button
v-for="item in alignment"
:key="item"
is-icon-only
:aria-label="item"
>
<ButtonGroupSeparator v-if="item !== 'left'" />
<DemoIcon :name="item" />
</Button>
</ButtonGroup>
</div>
<div class="demo-button-group-field">
<p class="demo-button-group-label">
Vertical
</p>
<ButtonGroup
orientation="vertical"
variant="tertiary"
>
<Button
v-for="item in alignment"
:key="item"
is-icon-only
:aria-label="item"
>
<ButtonGroupSeparator v-if="item !== 'left'" />
<DemoIcon :name="item" />
</Button>
</ButtonGroup>
</div>
</div>
</template>
<script setup lang="ts">
import { defineComponent, h } from 'vue'
import { Button, ButtonGroup, ButtonGroupSeparator } from '@heroui-vue/vue'
const alignment = ['left', 'center', 'right', 'justify'] as const
const iconPaths: Record<string, string[]> = {
left: ['M4 6h16', 'M4 10h10', 'M4 14h16', 'M4 18h10'],
center: ['M4 6h16', 'M7 10h10', 'M4 14h16', 'M7 18h10'],
right: ['M4 6h16', 'M10 10h10', 'M4 14h16', 'M10 18h10'],
justify: ['M4 6h16', 'M4 10h16', 'M4 14h16', 'M4 18h16'],
}
const DemoIcon = defineComponent({
props: {
name: {
type: String,
required: true,
},
},
setup(props) {
return () =>
h(
'svg',
{
viewBox: '0 0 24 24',
fill: 'none',
stroke: 'currentColor',
'stroke-width': '2',
'stroke-linecap': 'round',
'stroke-linejoin': 'round',
'aria-hidden': 'true',
},
iconPaths[props.name]?.map((d) => h('path', { d })) ?? [],
)
},
})
</script>
<style lang="less">
.demo-button-group-row {
display: flex;
flex-wrap: wrap;
align-items: flex-start;
gap: 1rem;
text-align: left;
}
.demo-button-group-field {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 0.5rem;
text-align: left;
}
.demo-button-group-label {
margin: 0;
color: var(--color-muted-foreground);
font-size: 0.875rem;
line-height: 1.25rem;
text-align: left;
}
</style>With Icons
With icons
Icon only buttons
<template>
<div class="demo-button-group-stack">
<div class="demo-button-group-field">
<p class="demo-button-group-label">
With icons
</p>
<ButtonGroup variant="secondary">
<Button>
<DemoIcon name="globe" />
Search
</Button>
<Button>
<ButtonGroupSeparator />
<DemoIcon name="plus" />
Add
</Button>
<Button>
<ButtonGroupSeparator />
<DemoIcon name="trash" />
Delete
</Button>
</ButtonGroup>
</div>
<div class="demo-button-group-field">
<p class="demo-button-group-label">
Icon only buttons
</p>
<ButtonGroup variant="tertiary">
<Button is-icon-only aria-label="Search">
<DemoIcon name="globe" />
</Button>
<Button is-icon-only aria-label="Add">
<ButtonGroupSeparator />
<DemoIcon name="plus" />
</Button>
<Button is-icon-only aria-label="Delete">
<ButtonGroupSeparator />
<DemoIcon name="trash" />
</Button>
</ButtonGroup>
</div>
</div>
</template>
<script setup lang="ts">
import { defineComponent, h } from 'vue'
import { Button, ButtonGroup, ButtonGroupSeparator } from '@heroui-vue/vue'
const iconPaths: Record<string, string[]> = {
globe: ['M12 21a9 9 0 1 0 0-18 9 9 0 0 0 0 18Z', 'M3.6 9h16.8', 'M3.6 15h16.8', 'M12 3a14 14 0 0 1 0 18', 'M12 3a14 14 0 0 0 0 18'],
plus: ['M12 5v14', 'M5 12h14'],
trash: ['M4 7h16', 'M10 11v6', 'M14 11v6', 'M6 7l1 13h10l1-13', 'M9 7V4h6v3'],
}
const DemoIcon = defineComponent({
props: {
name: {
type: String,
required: true,
},
},
setup(props) {
return () =>
h(
'svg',
{
viewBox: '0 0 24 24',
fill: 'none',
stroke: 'currentColor',
'stroke-width': '2',
'stroke-linecap': 'round',
'stroke-linejoin': 'round',
'aria-hidden': 'true',
},
iconPaths[props.name]?.map((d) => h('path', { d })) ?? [],
)
},
})
</script>
<style lang="less">
.demo-button-group-stack {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 1.5rem;
text-align: left;
}
.demo-button-group-field {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 0.5rem;
text-align: left;
}
.demo-button-group-label {
margin: 0;
color: var(--color-muted-foreground);
font-size: 0.875rem;
line-height: 1.25rem;
text-align: left;
}
</style>Full Width
<template>
<div class="demo-button-group-stack demo-button-group-fixed">
<ButtonGroup full-width>
<Button>First</Button>
<Button>
<ButtonGroupSeparator />
Second
</Button>
<Button>
<ButtonGroupSeparator />
Third
</Button>
</ButtonGroup>
<ButtonGroup full-width>
<Button
v-for="item in alignment"
:key="item"
is-icon-only
:aria-label="item"
>
<ButtonGroupSeparator v-if="item !== 'left'" />
<DemoIcon :name="item" />
</Button>
</ButtonGroup>
</div>
</template>
<script setup lang="ts">
import { defineComponent, h } from 'vue'
import { Button, ButtonGroup, ButtonGroupSeparator } from '@heroui-vue/vue'
const alignment = ['left', 'center', 'right'] as const
const iconPaths: Record<string, string[]> = {
left: ['M4 6h16', 'M4 10h10', 'M4 14h16', 'M4 18h10'],
center: ['M4 6h16', 'M7 10h10', 'M4 14h16', 'M7 18h10'],
right: ['M4 6h16', 'M10 10h10', 'M4 14h16', 'M10 18h10'],
}
const DemoIcon = defineComponent({
props: {
name: {
type: String,
required: true,
},
},
setup(props) {
return () =>
h(
'svg',
{
viewBox: '0 0 24 24',
fill: 'none',
stroke: 'currentColor',
'stroke-width': '2',
'stroke-linecap': 'round',
'stroke-linejoin': 'round',
'aria-hidden': 'true',
},
iconPaths[props.name]?.map((d) => h('path', { d })) ?? [],
)
},
})
</script>
<style lang="less">
.demo-button-group-stack {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 1.5rem;
text-align: left;
}
.demo-button-group-fixed {
width: 25rem;
max-width: 100%;
}
</style>Disabled State
All buttons disabled
Group disabled, but one button overrides
<template>
<div class="demo-button-group-stack">
<div class="demo-button-group-field">
<p class="demo-button-group-label">
All buttons disabled
</p>
<ButtonGroup is-disabled>
<Button>First</Button>
<Button>
<ButtonGroupSeparator />
Second
</Button>
<Button>
<ButtonGroupSeparator />
Third
</Button>
</ButtonGroup>
</div>
<div class="demo-button-group-field">
<p class="demo-button-group-label">
Group disabled, but one button overrides
</p>
<ButtonGroup is-disabled>
<Button>First</Button>
<Button>
<ButtonGroupSeparator />
Second
</Button>
<Button :is-disabled="false">
<ButtonGroupSeparator />
Third (enabled)
</Button>
</ButtonGroup>
</div>
</div>
</template>
<script setup lang="ts">
import { Button, ButtonGroup, ButtonGroupSeparator } from '@heroui-vue/vue'
</script>
<style lang="less">
.demo-button-group-stack {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 1.5rem;
text-align: left;
}
.demo-button-group-field {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 0.5rem;
text-align: left;
}
.demo-button-group-label {
margin: 0;
color: var(--color-muted-foreground);
font-size: 0.875rem;
line-height: 1.25rem;
text-align: left;
}
</style>Without Separator
<template>
<ButtonGroup>
<Button>First</Button>
<Button>Second</Button>
<Button>Third</Button>
</ButtonGroup>
</template>
<script setup lang="ts">
import { Button, ButtonGroup } from '@heroui-vue/vue'
</script>Styling
Passing Classes
vue
<template>
<ButtonGroup class="gap-2">
<Button>First</Button>
<Button>
<ButtonGroupSeparator />
Second
</Button>
<Button>
<ButtonGroupSeparator />
Third
</Button>
</ButtonGroup>
</template>CSS Classes
| Class | Description |
|---|---|
.button-group | Base button group container |
.button-group--horizontal | Horizontal orientation |
.button-group--vertical | Vertical orientation |
.button-group--full-width | Full width group and stretched child buttons |
.button-group__separator | Separator element between buttons |
Interactive States
| Selector | Description |
|---|---|
[data-disabled="true"] | Applied when the group is disabled |
[data-orientation="horizontal"] | Horizontal layout state |
[data-orientation="vertical"] | Vertical layout state |
.button-group .button[data-pressed="true"] | Pressed child buttons do not scale inside the group |
.button-group .button[data-focus-visible="true"] | Child focus ring is inset to stay inside connected edges |
API
ButtonGroup Props
| Prop | Type | Default | Description |
|---|---|---|---|
variant | 'primary' | 'secondary' | 'tertiary' | 'danger' | 'danger-soft' | 'outline' | 'ghost' | undefined | Variant applied to child buttons |
size | 'sm' | 'md' | 'lg' | undefined | Size applied to child buttons |
isDisabled | boolean | false | Whether child buttons inherit disabled state |
fullWidth | boolean | false | Whether the group and child buttons stretch to fill the container |
orientation | 'horizontal' | 'vertical' | 'horizontal' | Button layout orientation |
class | string | undefined | Additional classes for the group root |
ButtonGroupSeparator Props
| Prop | Type | Default | Description |
|---|---|---|---|
class | string | undefined | Additional classes for the separator |
Slots
| Component | Slot | Description |
|---|---|---|
ButtonGroup | default | Button group content |