<script setup>
import { ArrowLeft } from "@lucide/vue";
import { Button } from "@dicehub/kappa/components/button";
import { ButtonGroup } from "@dicehub/kappa/components/button-group";
</script>
<template>
<ButtonGroup aria-label="Run actions">
<ButtonGroup.Root aria-label="Run navigation">
<Button :icon="ArrowLeft" shape="square" variant="outline" aria-label="Back to runs" />
</ButtonGroup.Root>
<ButtonGroup.Root aria-label="Run preparation">
<Button variant="outline">Save</Button>
<Button variant="outline">Validate</Button>
</ButtonGroup.Root>
<ButtonGroup.Root aria-label="Run output">
<Button variant="outline">Run</Button>
<Button variant="outline">Export</Button>
</ButtonGroup.Root>
</ButtonGroup>
</template>Installation
Barrel
import {
ButtonGroup,
ButtonGroupRoot,
ButtonGroupSeparator,
ButtonGroupText,
} from "@dicehub/kappa";Granular
import {
ButtonGroup,
ButtonGroupRoot,
ButtonGroupSeparator,
ButtonGroupText,
} from "@dicehub/kappa/components/button-group";Usage
Give the group an accessible name when its purpose is not clear from nearby content. Each child keeps its native action or link behavior.
<script setup>
import { Button } from "@dicehub/kappa/components/button";
import { ButtonGroup } from "@dicehub/kappa/components/button-group";
</script>
<template>
<ButtonGroup aria-label="Case actions">
<Button variant="outline">Save</Button>
<Button variant="outline">Duplicate</Button>
<Button variant="outline">Archive</Button>
</ButtonGroup>
</template>Composition
ButtonGroup.Root
├── Button / LinkButton / native input
├── ButtonGroup.Separator
└── ButtonGroup.TextPlace Button, LinkButton, ButtonGroup.Text, orButtonGroup.Separator directly inside the root. Nest roots when separate connected clusters need a small gap. Native inputs can join the group withdata-slot="input". The Menu, Select, and Popover examples use Ark UI directly because Kappa does not yet provide styled wrappers for those primitives.
ButtonGroup— semantic group and visual axis.ButtonGroup.Text— static context for adjacent controls.ButtonGroup.Separator— accessible boundary between actions.
Examples
Basic
Connected outline buttons share one border and retain separate actions.
<script setup>
import { Button } from "@dicehub/kappa/components/button";
import { ButtonGroup } from "@dicehub/kappa/components/button-group";
</script>
<template>
<ButtonGroup aria-label="Result actions">
<Button variant="outline">Open</Button>
<Button variant="outline">Compare</Button>
<Button variant="outline">Export</Button>
</ButtonGroup>
</template>Orientation
Horizontal is the default. Vertical changes only the visual axis; native Tab order stays unchanged.
<script setup>
import { Minus, Plus } from "@lucide/vue";
import { Button } from "@dicehub/kappa/components/button";
import { ButtonGroup } from "@dicehub/kappa/components/button-group";
</script>
<template>
<ButtonGroup aria-label="Horizontal zoom controls">
<Button :icon="Minus" shape="square" variant="outline" aria-label="Zoom out" />
<Button :icon="Plus" shape="square" variant="outline" aria-label="Zoom in" />
</ButtonGroup>
<ButtonGroup orientation="vertical" aria-label="Vertical zoom controls">
<Button :icon="Plus" shape="square" variant="outline" aria-label="Increase scale" />
<Button :icon="Minus" shape="square" variant="outline" aria-label="Decrease scale" />
</ButtonGroup>
</template>Sizes
Set the same size on each child. Button Group does not override child props.
<script setup>
import { Button } from "@dicehub/kappa/components/button";
import { ButtonGroup } from "@dicehub/kappa/components/button-group";
</script>
<template>
<ButtonGroup v-for="size in ['xs', 'sm', 'base', 'lg']" :key="size" :aria-label="size + ' actions'">
<Button :size="size" variant="outline">Mesh</Button>
<Button :size="size" variant="outline">Solve</Button>
<Button :size="size" variant="outline">Review</Button>
</ButtonGroup>
</template>Nested
Nest roots to separate related clusters without introducing decorative containers.
<script setup>
import { Redo2, Undo2 } from "@lucide/vue";
import { Button } from "@dicehub/kappa/components/button";
import { ButtonGroup } from "@dicehub/kappa/components/button-group";
</script>
<template>
<ButtonGroup aria-label="Editor actions">
<ButtonGroup.Root aria-label="History actions">
<Button :icon="Undo2" shape="square" variant="outline" aria-label="Undo" />
<Button :icon="Redo2" shape="square" variant="outline" aria-label="Redo" />
</ButtonGroup.Root>
<ButtonGroup.Root aria-label="Editor state actions">
<Button variant="outline">Apply</Button>
<Button variant="outline">Reset</Button>
</ButtonGroup.Root>
</ButtonGroup>
</template>The second composition keeps the message field and inline voice indicator in one input surface, separated from the attachment action.
<script setup>
import { AudioLines, Plus } from "@lucide/vue";
import { Button } from "@dicehub/kappa/components/button";
import { ButtonGroup } from "@dicehub/kappa/components/button-group";
</script>
<template>
<ButtonGroup aria-label="Message composer">
<ButtonGroup.Root aria-label="Attachment actions">
<Button :icon="Plus" shape="square" variant="outline" aria-label="Add attachment" />
</ButtonGroup.Root>
<ButtonGroup.Root aria-label="Message input">
<div
class="button-group-native-input-group button-group-native-input-group--nested"
data-slot="input-group"
role="group"
aria-label="Message and voice controls"
>
<input
class="button-group-native-input-group__field"
data-slot="input-group-control"
type="text"
aria-label="Message"
placeholder="Send a message..."
/>
<div
class="button-group-native-input-group__addon"
data-slot="input-group-addon"
data-align="inline-end"
role="img"
aria-label="Voice Mode"
title="Voice Mode"
>
<AudioLines aria-hidden="true" />
</div>
</div>
</ButtonGroup.Root>
</ButtonGroup>
</template>
<style scoped>
.button-group-native-input-group {
box-sizing: border-box;
display: flex;
inline-size: clamp(10rem, 56vw, 18rem);
min-inline-size: 0;
block-size: 2rem;
align-items: center;
border: 1px solid var(--kappa-line, #e3e6eb);
border-radius: var(--kappa-button-group-radius, 0.5rem);
background: var(--kappa-control, #ffffff);
color: var(--kappa-default, #17191f);
}
.button-group-native-input-group:focus-within {
outline: 2px solid var(--kappa-focus, #4c63ff);
outline-offset: 2px;
}
.button-group-native-input-group[data-active="true"] {
border-color: var(--kappa-warning, #d97706);
}
.button-group-native-input-group--nested {
inline-size: min(14.5625rem, 60vw);
}
.button-group-native-input-group__field {
min-inline-size: 0;
block-size: 100%;
flex: 1 1 auto;
padding-inline: 0.625rem;
border: 0;
background: transparent;
color: inherit;
font-family: var(--kappa-font-sans, inherit);
font-size: 0.8125rem;
outline: 0;
}
.button-group-native-input-group__field::placeholder {
color: var(--kappa-muted, #9aa2ae);
}
.button-group-native-input-group__field:disabled {
cursor: not-allowed;
}
.button-group-native-input-group__addon {
display: flex;
flex: none;
align-items: center;
justify-content: center;
padding-inline-end: 0.5rem;
color: var(--kappa-subtle, #5f6875);
user-select: none;
}
.button-group-native-input-group__addon svg {
inline-size: 1rem;
block-size: 1rem;
}
.button-group-native-input-group__action[data-active="true"] {
background: var(--kappa-warning-tint, #fff4e5);
color: var(--kappa-warning-text, #b54708);
}
.button-group-native-input-group__action {
flex: none;
color: var(--kappa-subtle, #5f6875);
}
.button-group-native-input-group__action:focus-visible {
outline: 0;
}
@media (forced-colors: active) {
.button-group-native-input-group { border-color: ButtonText; }
.button-group-native-input-group:focus-within { outline-color: Highlight; }
}
</style>Separator
Use a separator with borderless variants such as ghost or primary. Outline buttons already provide a single seam and do not need another line. Setorientation="horizontal" inside a vertical group.
<script setup>
import { Button } from "@dicehub/kappa/components/button";
import { ButtonGroup } from "@dicehub/kappa/components/button-group";
</script>
<template>
<ButtonGroup aria-label="Clipboard actions">
<Button variant="ghost">Copy</Button>
<ButtonGroup.Separator />
<Button variant="ghost">Paste</Button>
</ButtonGroup>
<ButtonGroup orientation="vertical" aria-label="Case lifecycle">
<Button variant="ghost">Prepare</Button>
<ButtonGroup.Separator orientation="horizontal" />
<Button variant="ghost">Run</Button>
</ButtonGroup>
</template>Split
This example uses split geometry for two independent actions. It does not provide a menu or claim popup behavior; compose a real menu trigger when one is available.
<script setup>
import { Copy } from "@lucide/vue";
import { Button } from "@dicehub/kappa/components/button";
import { ButtonGroup } from "@dicehub/kappa/components/button-group";
</script>
<template>
<ButtonGroup aria-label="Run case">
<Button variant="primary">Run case</Button>
<ButtonGroup.Separator />
<Button
:icon="Copy"
shape="square"
variant="primary"
aria-label="Duplicate case and run"
/>
</ButtonGroup>
</template>Input
A native input can join a Button when it uses data-slot="input". Keep the input labelled and apply product input styles in the consumer.
<script setup>
import { Search } from "@lucide/vue";
import { Button } from "@dicehub/kappa/components/button";
import { ButtonGroup } from "@dicehub/kappa/components/button-group";
</script>
<template>
<ButtonGroup aria-label="Case search">
<input
class="button-group-native-input"
data-slot="input"
type="search"
aria-label="Search cases"
placeholder="Search..."
/>
<Button :icon="Search" shape="square" variant="outline" aria-label="Search" />
</ButtonGroup>
</template>
<style scoped>
.button-group-native-input {
box-sizing: border-box;
inline-size: clamp(8rem, 48vw, 14rem);
min-inline-size: 0;
block-size: 2rem;
flex: 1 1 auto;
appearance: none;
padding-block: 0;
padding-inline: 0.625rem;
border: 1px solid var(--kappa-line, #e3e6eb);
border-radius: var(--kappa-button-group-radius, 0.5rem);
background: var(--kappa-control, #ffffff);
color: var(--kappa-default, #17191f);
font-family: var(--kappa-font-sans, inherit);
font-size: 0.8125rem;
line-height: 1rem;
}
.button-group-native-input::placeholder {
color: var(--kappa-muted, #9aa2ae);
}
.button-group-native-input:focus-visible {
outline: 2px solid var(--kappa-focus, #4c63ff);
outline-offset: 2px;
}
.button-group-native-input:disabled {
cursor: not-allowed;
opacity: var(--kappa-disabled-opacity, 0.56);
}
@media (forced-colors: active) {
.button-group-native-input { border-color: ButtonText; }
.button-group-native-input:focus-visible { outline-color: Highlight; }
}
</style>Input Group
Nest a local input surface when the field owns an inline action. This example keeps the voice toggle state and accessible name on a real Button.
<script setup>
import { AudioLines, Plus } from "@lucide/vue";
import { ref } from "vue";
import { Button } from "@dicehub/kappa/components/button";
import { ButtonGroup } from "@dicehub/kappa/components/button-group";
const voiceEnabled = ref(false);
</script>
<template>
<ButtonGroup aria-label="Message composer">
<ButtonGroup.Root aria-label="Attachment actions">
<Button :icon="Plus" shape="square" variant="outline" aria-label="Add attachment" />
</ButtonGroup.Root>
<ButtonGroup.Root aria-label="Message controls">
<div
class="button-group-native-input-group"
data-slot="input-group"
:data-active="voiceEnabled ? 'true' : undefined"
>
<input
class="button-group-native-input-group__field"
data-slot="input-group-control"
type="text"
aria-label="Message"
:disabled="voiceEnabled"
:placeholder="voiceEnabled ? 'Record and send audio...' : 'Send a message...'"
/>
<Button
:icon="AudioLines"
class="button-group-native-input-group__action"
shape="square"
size="xs"
variant="ghost"
:aria-label="voiceEnabled ? 'Stop voice input' : 'Start voice input'"
:aria-pressed="voiceEnabled"
:data-active="voiceEnabled ? 'true' : undefined"
:title="voiceEnabled ? 'Stop voice input' : 'Start voice input'"
@click="voiceEnabled = !voiceEnabled"
/>
</div>
</ButtonGroup.Root>
</ButtonGroup>
</template>
<style scoped>
.button-group-native-input-group {
box-sizing: border-box;
display: flex;
inline-size: clamp(10rem, 56vw, 18rem);
min-inline-size: 0;
block-size: 2rem;
align-items: center;
border: 1px solid var(--kappa-line, #e3e6eb);
border-radius: var(--kappa-button-group-radius, 0.5rem);
background: var(--kappa-control, #ffffff);
color: var(--kappa-default, #17191f);
}
.button-group-native-input-group:focus-within {
outline: 2px solid var(--kappa-focus, #4c63ff);
outline-offset: 2px;
}
.button-group-native-input-group[data-active="true"] {
border-color: var(--kappa-warning, #d97706);
}
.button-group-native-input-group--nested {
inline-size: min(14.5625rem, 60vw);
}
.button-group-native-input-group__field {
min-inline-size: 0;
block-size: 100%;
flex: 1 1 auto;
padding-inline: 0.625rem;
border: 0;
background: transparent;
color: inherit;
font-family: var(--kappa-font-sans, inherit);
font-size: 0.8125rem;
outline: 0;
}
.button-group-native-input-group__field::placeholder {
color: var(--kappa-muted, #9aa2ae);
}
.button-group-native-input-group__field:disabled {
cursor: not-allowed;
}
.button-group-native-input-group__addon {
display: flex;
flex: none;
align-items: center;
justify-content: center;
padding-inline-end: 0.5rem;
color: var(--kappa-subtle, #5f6875);
user-select: none;
}
.button-group-native-input-group__addon svg {
inline-size: 1rem;
block-size: 1rem;
}
.button-group-native-input-group__action[data-active="true"] {
background: var(--kappa-warning-tint, #fff4e5);
color: var(--kappa-warning-text, #b54708);
}
.button-group-native-input-group__action {
flex: none;
color: var(--kappa-subtle, #5f6875);
}
.button-group-native-input-group__action:focus-visible {
outline: 0;
}
@media (forced-colors: active) {
.button-group-native-input-group { border-color: ButtonText; }
.button-group-native-input-group:focus-within { outline-color: Highlight; }
}
</style>Dropdown Menu
Use Ark Menu for popup behavior. Keep its positioner outside ButtonGroup so it does not become part of the joined border sequence.
Select
A Select trigger, amount field, and send action form one compact transfer control. Ark owns option navigation and selection.
<script setup>
import { Check, ChevronDown, ArrowRight } from "@lucide/vue";
import { Select, createListCollection } from "@ark-ui/vue/select";
import { ref } from "vue";
import { Button } from "@dicehub/kappa/components/button";
import { ButtonGroup } from "@dicehub/kappa/components/button-group";
const currencyItems = [
{ label: "$", value: "$", name: "US Dollar" },
{ label: "€", value: "€", name: "Euro" },
{ label: "£", value: "£", name: "British Pound" },
];
const currencies = createListCollection({ items: currencyItems });
const currency = ref(["$"]);
</script>
<template>
<Select.Root
v-model="currency"
:collection="currencies"
name="currency"
:positioning="{ placement: 'bottom-start', gutter: 6 }"
>
<ButtonGroup aria-label="Transfer amount">
<ButtonGroup.Root aria-label="Amount and currency">
<Select.Trigger as-child>
<Button :icon="ChevronDown" icon-position="inline-end" variant="outline" aria-label="Currency">
<Select.ValueText placeholder="$" />
</Button>
</Select.Trigger>
<input
class="button-group-native-input button-group-amount-input"
data-slot="input"
type="text"
inputmode="decimal"
aria-label="Amount"
placeholder="10.00"
/>
</ButtonGroup.Root>
<ButtonGroup.Root aria-label="Transfer actions">
<Button :icon="ArrowRight" shape="square" variant="outline" aria-label="Send amount" />
</ButtonGroup.Root>
</ButtonGroup>
<Select.HiddenSelect />
<Select.Positioner class="button-group-positioner">
<Select.Content class="button-group-surface button-group-select" aria-label="Currencies">
<Select.Item
v-for="item in currencyItems"
:key="item.value"
:item="item"
class="button-group-option"
>
<Select.ItemText>
<span class="button-group-select__symbol">{{ item.label }}</span>
<span>{{ item.name }}</span>
</Select.ItemText>
<Select.ItemIndicator class="button-group-select__indicator">
<Check aria-hidden="true" />
</Select.ItemIndicator>
</Select.Item>
</Select.Content>
</Select.Positioner>
</Select.Root>
</template>
<style scoped>
.button-group-native-input {
box-sizing: border-box;
inline-size: clamp(8rem, 48vw, 14rem);
min-inline-size: 0;
block-size: 2rem;
flex: 1 1 auto;
appearance: none;
padding-block: 0;
padding-inline: 0.625rem;
border: 1px solid var(--kappa-line, #e3e6eb);
border-radius: var(--kappa-button-group-radius, 0.5rem);
background: var(--kappa-control, #ffffff);
color: var(--kappa-default, #17191f);
font-family: var(--kappa-font-sans, inherit);
font-size: 0.8125rem;
line-height: 1rem;
}
.button-group-native-input::placeholder {
color: var(--kappa-muted, #9aa2ae);
}
.button-group-native-input:focus-visible {
outline: 2px solid var(--kappa-focus, #4c63ff);
outline-offset: 2px;
}
.button-group-native-input:disabled {
cursor: not-allowed;
opacity: var(--kappa-disabled-opacity, 0.56);
}
@media (forced-colors: active) {
.button-group-native-input { border-color: ButtonText; }
.button-group-native-input:focus-visible { outline-color: Highlight; }
}
.button-group-positioner {
z-index: 30;
}
.button-group-surface {
box-sizing: border-box;
min-inline-size: 12rem;
border: 1px solid var(--kappa-line, #e3e6eb);
border-radius: 0.625rem;
background: var(--kappa-control, #ffffff);
color: var(--kappa-default, #17191f);
font-family: var(--kappa-font-sans, inherit);
font-size: 0.8125rem;
}
.button-group-surface[hidden] {
display: none;
}
.button-group-option {
display: flex;
min-block-size: 2rem;
align-items: center;
gap: 0.5rem;
padding-inline: 0.625rem;
border-radius: 0.375rem;
cursor: default;
outline: none;
}
.button-group-option[data-highlighted] {
background: var(--kappa-overlay, #e9edf2);
}
.button-group-option svg {
inline-size: 1rem;
block-size: 1rem;
flex: none;
}
.button-group-option--danger {
color: var(--kappa-danger-text, #b42318);
}
.button-group-option--danger[data-highlighted] {
background: var(--kappa-danger-tint, #fee4e2);
}
.button-group-separator {
block-size: 1px;
margin-block: 0.375rem;
background: var(--kappa-line, #e3e6eb);
}
.button-group-amount-input { inline-size: clamp(6rem, 28vw, 9rem); }
.button-group-select { min-inline-size: 11rem; padding: 0.375rem; }
.button-group-select__symbol { inline-size: 1.25rem; font-family: var(--kappa-font-mono, monospace); }
.button-group-select__indicator { margin-inline-start: auto; }
</style>Popover
Use Ark Popover for focus, outside-interaction, and Escape handling. The positioner stays beside the ButtonGroup rather than inside it.
<script setup>
import { Bot, ChevronDown } from "@lucide/vue";
import { Popover } from "@ark-ui/vue/popover";
import { Button } from "@dicehub/kappa/components/button";
import { ButtonGroup } from "@dicehub/kappa/components/button-group";
</script>
<template>
<Popover.Root :portalled="false" :positioning="{ placement: 'bottom-end', gutter: 6 }">
<ButtonGroup aria-label="Copilot actions">
<Button :icon="Bot" variant="outline">Copilot</Button>
<Popover.Trigger as-child>
<Button :icon="ChevronDown" shape="square" variant="outline" aria-label="Open Copilot task" />
</Popover.Trigger>
</ButtonGroup>
<Popover.Positioner class="button-group-positioner">
<Popover.Content class="button-group-surface button-group-popover">
<Popover.Title class="button-group-popover__title">Start a new task with Copilot</Popover.Title>
<Popover.Description class="button-group-popover__description">
Describe your task in natural language.
</Popover.Description>
<label class="button-group-popover__label" for="copilot-task">Task description</label>
<textarea
id="copilot-task"
class="button-group-popover__textarea"
rows="4"
placeholder="I need to..."
></textarea>
<p class="button-group-popover__hint">Copilot will open a pull request for review.</p>
</Popover.Content>
</Popover.Positioner>
</Popover.Root>
</template>
<style scoped>
.button-group-positioner {
z-index: 30;
}
.button-group-surface {
box-sizing: border-box;
min-inline-size: 12rem;
border: 1px solid var(--kappa-line, #e3e6eb);
border-radius: 0.625rem;
background: var(--kappa-control, #ffffff);
color: var(--kappa-default, #17191f);
font-family: var(--kappa-font-sans, inherit);
font-size: 0.8125rem;
}
.button-group-surface[hidden] {
display: none;
}
.button-group-popover {
display: grid;
inline-size: min(20rem, calc(100vw - 2rem));
gap: 0.75rem;
padding: 1rem;
}
.button-group-popover__title { font-size: 0.875rem; font-weight: 650; }
.button-group-popover__description,
.button-group-popover__hint { margin: 0; color: var(--kappa-subtle, #5f6875); line-height: 1.4; }
.button-group-popover__label { font-weight: 600; }
.button-group-popover__textarea {
box-sizing: border-box;
inline-size: 100%;
min-block-size: 6rem;
resize: vertical;
padding: 0.625rem;
border: 1px solid var(--kappa-line, #e3e6eb);
border-radius: 0.5rem;
background: var(--kappa-control, #ffffff);
color: var(--kappa-default, #17191f);
font: inherit;
}
.button-group-popover__textarea::placeholder { color: var(--kappa-muted, #9aa2ae); }
.button-group-popover__textarea:focus-visible {
outline: 2px solid var(--kappa-focus, #4c63ff);
outline-offset: 2px;
}
</style>Text and asChild
Text supplies non-interactive context. Use asChild to keep a more suitable semantic child while Kappa applies the connected treatment. Text aligns with base-size controls; consumers remain responsible for the size of every interactive child.
<script setup>
import { Gauge } from "@lucide/vue";
import { Button } from "@dicehub/kappa/components/button";
import { ButtonGroup } from "@dicehub/kappa/components/button-group";
</script>
<template>
<ButtonGroup aria-label="Time step limit">
<ButtonGroup.Text>Delta t</ButtonGroup.Text>
<Button variant="outline">0.002 s</Button>
</ButtonGroup>
<ButtonGroup aria-label="Courant limit">
<ButtonGroup.Text as-child>
<strong><Gauge aria-hidden="true" /> Courant</strong>
</ButtonGroup.Text>
<Button variant="outline">0.8</Button>
</ButtonGroup>
</template>Link Buttons
LinkButton remains a real anchor when navigation belongs beside related actions.
<script setup>
import { Button, LinkButton } from "@dicehub/kappa/components/button";
import { ButtonGroup } from "@dicehub/kappa/components/button-group";
</script>
<template>
<ButtonGroup aria-label="Run navigation">
<LinkButton href="/runs/042" variant="outline">Summary</LinkButton>
<LinkButton href="/runs/042/fields" variant="outline">Fields</LinkButton>
<Button variant="outline">Refresh</Button>
</ButtonGroup>
</template>RTL
Kappa uses logical borders and corner radii. Set the surrounding direction and choose directional icons that match the reading direction.
<script setup>
import { ArrowRight, Plus } from "@lucide/vue";
import { Button } from "@dicehub/kappa/components/button";
import { ButtonGroup } from "@dicehub/kappa/components/button-group";
</script>
<template>
<div dir="rtl">
<ButtonGroup aria-label="إجراءات الحالة">
<Button :icon="ArrowRight" shape="square" variant="outline" aria-label="العودة" />
<Button :icon="Plus" variant="outline">حالة جديدة</Button>
<Button variant="outline">تصدير</Button>
</ButtonGroup>
</div>
</template>Accessibility
- The root has
role="group". Addaria-labeloraria-labelledbywhen context does not name it. - Tab and Shift+Tab move through every control. Button Group does not add arrow-key navigation or roving focus.
- Every icon-only button needs its own concise accessible name.
- Separator is intentionally semantic, matching the official Base Separator contract. It exposes its axis with
role="separator"andaria-orientation. - Use toggle controls for pressed or selected states. Button Group manages layout only.
- Focus, disabled, loading, dark-mode, and reduced-motion behavior stay owned by each child component.
API Reference
ButtonGroup
Renders a semantic group and forwards remaining HTML attributes and listeners.
| Prop | Type | Default | Description |
|---|---|---|---|
orientation | "horizontal" | "vertical" | "horizontal" | Sets the visual axis. It does not add arrow-key navigation. |
ButtonGroup.Text
| Prop | Type | Default | Description |
|---|---|---|---|
asChild | boolean | false | Merges text styling and attributes onto one semantic child. |
ButtonGroup.Separator
| Prop | Type | Default | Description |
|---|---|---|---|
orientation | "horizontal" | "vertical" | "vertical" | Sets the separator axis. Use horizontal inside a vertical group. |
Exports
| Export | Description |
|---|---|
ButtonGroup | Root component with Root, Text, and Separator compound parts. |
ButtonGroupRoot | Unaugmented semantic group component. |
ButtonGroupText | Static text or an asChild semantic element inside the group. |
ButtonGroupSeparator | Accessible visual separator between controls. |
BUTTON_GROUP_ORIENTATIONS | Readonly horizontal and vertical option list. |
BUTTON_GROUP_DEFAULT_ORIENTATION | Public horizontal root default. |
BUTTON_GROUP_SEPARATOR_DEFAULT_ORIENTATION | Public vertical separator default. |
isButtonGroupOrientation | Runtime orientation guard. |
resolveButtonGroupOrientation | Runtime orientation resolver with a safe fallback. |
resolveButtonGroupSeparatorOrientation | Resolves an explicit separator axis with a vertical fallback. |
ButtonGroup*Props / ButtonGroup*Slots | Public Vue part contracts. |
ButtonGroupOrientation | Public orientation union. |