Skip to content

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

ClassDescription
.button-groupBase button group container
.button-group--horizontalHorizontal orientation
.button-group--verticalVertical orientation
.button-group--full-widthFull width group and stretched child buttons
.button-group__separatorSeparator element between buttons

Interactive States

SelectorDescription
[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

PropTypeDefaultDescription
variant'primary' | 'secondary' | 'tertiary' | 'danger' | 'danger-soft' | 'outline' | 'ghost'undefinedVariant applied to child buttons
size'sm' | 'md' | 'lg'undefinedSize applied to child buttons
isDisabledbooleanfalseWhether child buttons inherit disabled state
fullWidthbooleanfalseWhether the group and child buttons stretch to fill the container
orientation'horizontal' | 'vertical''horizontal'Button layout orientation
classstringundefinedAdditional classes for the group root

ButtonGroupSeparator Props

PropTypeDefaultDescription
classstringundefinedAdditional classes for the separator

Slots

ComponentSlotDescription
ButtonGroupdefaultButton group content

Released under the Apache-2.0 License.