Keyboard shortcut: Ctrl + K
Get started

Button group

Joins buttons into one segmented control: merged borders, rounded corners at the ends only. The row is one object, so it is the group that decides how it is drawn, from whether the buttons are joined at all down to the variant, the tone, the size and the density every button inside picks up.

Usage

vue
<script setup lang="ts">
import { VButton, VButtonGroup } from 'vectis-ui'
</script>

<template>
  <VButtonGroup variant="outline" tone="neutral" label="Text alignment">
    <VButton>Left</VButton>
    <VButton>Centre</VButton>
    <VButton>Right</VButton>
  </VButtonGroup>
</template>

Examples

Variants and tones

variant and tone are named once on the group. The variant wins over whatever a button inside was given; the tone is only a fallback.

vue
<script setup lang="ts">
import { VButton, VButtonGroup } from 'vectis-ui'

const variants = ['solid', 'soft', 'outline', 'ghost'] as const
const tones = ['accent', 'neutral', 'danger'] as const
</script>

<template>
  <div v-for="tone in tones" :key="tone" class="row">
    <VButtonGroup
      v-for="variant in variants"
      :key="variant"
      :variant="variant"
      :tone="tone"
      :label="`${variant} ${tone}`"
    >
      <VButton>Day</VButton>
      <VButton>Week</VButton>
      <VButton>Month</VButton>
    </VButtonGroup>
  </div>
</template>

<style scoped>
.row {
  display: flex;
  flex-wrap: wrap;
  gap: var(--vectis-space-4);
}
</style>

One segment, another tone

A segment keeps its own tone against the group, which is how a neutral row holds a single destructive action.

vue
<script setup lang="ts">
import { VButton, VButtonGroup } from 'vectis-ui'
</script>

<template>
  <div class="column">
    <VButtonGroup variant="outline" tone="neutral" label="Row actions, outline">
      <VButton>Rename</VButton>
      <VButton>Duplicate</VButton>
      <VButton tone="danger">Delete</VButton>
    </VButtonGroup>

    <VButtonGroup variant="soft" tone="neutral" label="Row actions, tonal">
      <VButton>Rename</VButton>
      <VButton>Duplicate</VButton>
      <VButton tone="danger">Delete</VButton>
    </VButtonGroup>
  </div>
</template>

<style scoped>
.column {
  display: flex;
  flex-direction: column;
  align-items: start;
  gap: var(--vectis-space-3);
}
</style>

Orientation

vertical stacks the segments in a column instead of a row, the joining following the axis.

vue
<script setup lang="ts">
import { VButton, VButtonGroup } from 'vectis-ui'
</script>

<template>
  <VButtonGroup variant="outline" tone="neutral" label="Range, as a row">
    <VButton>Day</VButton>
    <VButton>Week</VButton>
    <VButton>Month</VButton>
  </VButtonGroup>

  <VButtonGroup orientation="vertical" variant="outline" tone="neutral" label="Range, as a column">
    <VButton>Day</VButton>
    <VButton>Week</VButton>
    <VButton>Month</VButton>
  </VButtonGroup>
</template>

Detached

detached leaves the buttons apart, each keeping its own corners and borders, while the group still hands its appearance props down.

vue
<script setup lang="ts">
import { VButton, VButtonGroup } from 'vectis-ui'
</script>

<template>
  <div class="column">
    <VButtonGroup variant="outline" tone="neutral" label="Joined">
      <VButton>Day</VButton>
      <VButton>Week</VButton>
      <VButton>Month</VButton>
    </VButtonGroup>

    <VButtonGroup detached variant="outline" tone="neutral" label="Detached">
      <VButton>Day</VButton>
      <VButton>Week</VButton>
      <VButton>Month</VButton>
    </VButtonGroup>
  </div>
</template>

<style scoped>
.column {
  display: flex;
  flex-direction: column;
  align-items: start;
  gap: var(--vectis-space-3);
}
</style>

Seamless

seamless removes the lines drawn at each joint, leaving a single frame with its outer edges intact.

vue
<script setup lang="ts">
import { VButton, VButtonGroup } from 'vectis-ui'
</script>

<template>
  <div class="column">
    <VButtonGroup variant="outline" tone="neutral" label="With lines">
      <VButton>Day</VButton>
      <VButton>Week</VButton>
      <VButton>Month</VButton>
    </VButtonGroup>

    <VButtonGroup seamless variant="outline" tone="neutral" label="Seamless">
      <VButton>Day</VButton>
      <VButton>Week</VButton>
      <VButton>Month</VButton>
    </VButtonGroup>
  </div>
</template>

<style scoped>
.column {
  display: flex;
  flex-direction: column;
  align-items: start;
  gap: var(--vectis-space-3);
}
</style>

Elevated

elevated gives the shadow to the row rather than to each segment. Detached, each button casts its own again.

vue
<script setup lang="ts">
import { VButton, VButtonGroup } from 'vectis-ui'
</script>

<template>
  <div class="column">
    <VButtonGroup variant="ghost" tone="neutral" label="Flat">
      <VButton>Day</VButton>
      <VButton>Week</VButton>
      <VButton>Month</VButton>
    </VButtonGroup>

    <VButtonGroup elevated variant="ghost" tone="neutral" label="Raised">
      <VButton>Day</VButton>
      <VButton>Week</VButton>
      <VButton>Month</VButton>
    </VButtonGroup>

    <VButtonGroup elevated detached variant="ghost" tone="neutral" label="Raised, detached">
      <VButton>Day</VButton>
      <VButton>Week</VButton>
      <VButton>Month</VButton>
    </VButtonGroup>
  </div>
</template>

<style scoped>
.column {
  display: flex;
  flex-direction: column;
  align-items: start;
  gap: var(--vectis-space-4);
}
</style>

Sizes

size sets the height of every segment: 24, 32, 40, 48 or 56 pixels. It wins over whatever a button inside was given.

vue
<script setup lang="ts">
import { VButton, VButtonGroup } from 'vectis-ui'

const sizes = ['xs', 'sm', 'md', 'lg', 'xl'] as const
</script>

<template>
  <div class="column">
    <VButtonGroup
      v-for="size in sizes"
      :key="size"
      :size="size"
      variant="outline"
      tone="neutral"
      :label="`Range, ${size}`"
    >
      <VButton>Day</VButton>
      <VButton>Week</VButton>
      <VButton>Month</VButton>
    </VButtonGroup>
  </div>
</template>

<style scoped>
.column {
  display: flex;
  flex-direction: column;
  align-items: start;
  gap: var(--vectis-space-3);
}
</style>

Compact

compact takes 4px off the height of every segment.

vue
<script setup lang="ts">
import { VButton, VButtonGroup } from 'vectis-ui'

const sizes = ['sm', 'md', 'lg'] as const
</script>

<template>
  <div v-for="size in sizes" :key="size" class="row">
    <VButtonGroup :size="size" variant="outline" tone="neutral" :label="size">
      <VButton>Day</VButton>
      <VButton>Week</VButton>
      <VButton>Month</VButton>
    </VButtonGroup>

    <VButtonGroup :size="size" compact variant="outline" tone="neutral" :label="`${size} compact`">
      <VButton>Day</VButton>
      <VButton>Week</VButton>
      <VButton>Month</VButton>
    </VButtonGroup>
  </div>
</template>

<style scoped>
.row {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--vectis-space-4);
}
</style>

Full width

fullWidth stretches the row across its parent and gives every segment an equal share of that width.

vue
<script setup lang="ts">
import { VButton, VButtonGroup } from 'vectis-ui'
</script>

<template>
  <div class="column">
    <VButtonGroup variant="outline" tone="neutral" label="As wide as its labels">
      <VButton>Day</VButton>
      <VButton>Week</VButton>
      <VButton>Month</VButton>
    </VButtonGroup>

    <VButtonGroup full-width variant="outline" tone="neutral" label="Filling the column">
      <VButton>Day</VButton>
      <VButton>Week</VButton>
      <VButton>Month</VButton>
    </VButtonGroup>
  </div>
</template>

<style scoped>
/* A column narrower than the page, so that filling it is something to see. The rows are
   aligned to the start, which is what leaves the first at the width of its own labels. */
.column {
  display: flex;
  flex-direction: column;
  align-items: start;
  gap: var(--vectis-space-3);
  inline-size: 320px;
}
</style>

With icons

Segments carry iconStart and iconEnd the way any button does. A VIconButton is a segment too, and still needs its label.

vue
<script setup lang="ts">
import { VButton, VButtonGroup, VIconButton } from 'vectis-ui'
import {
  arrow_downward as arrowDownward,
  arrow_upward as arrowUpward,
  description,
  image,
  swap_vert as swapVert,
  table_chart as tableChart,
} from 'vectis-ui/icons'
</script>

<template>
  <VButtonGroup variant="outline" tone="neutral" label="View">
    <VButton :icon-start="description">List</VButton>
    <VButton :icon-start="tableChart">Table</VButton>
    <VButton :icon-start="image">Gallery</VButton>
  </VButtonGroup>

  <VButtonGroup role="toolbar" variant="outline" tone="neutral" label="Sort">
    <VIconButton :icon="arrowUpward" label="Ascending" />
    <VIconButton :icon="swapVert" label="Unsorted" />
    <VIconButton :icon="arrowDownward" label="Descending" />
  </VButtonGroup>
</template>

A segment given an href renders an <a>. A disabled link has its address dropped, which leaves it neither focusable nor followable.

vue
<script setup lang="ts">
import { VButton, VButtonGroup } from 'vectis-ui'
</script>

<template>
  <VButtonGroup variant="outline" tone="neutral" label="Sections of this page">
    <VButton href="#usage">Usage</VButton>
    <VButton href="#examples">Examples</VButton>
    <VButton href="#api">API</VButton>
    <VButton href="#usage" disabled>Changelog</VButton>
  </VButtonGroup>
</template>

States

disabled on the group adds up with each button's own: a segment cannot opt back in once the row is off. loading stays the button's own.

vue
<script setup lang="ts">
import { VButton, VButtonGroup } from 'vectis-ui'
</script>

<template>
  <div class="column">
    <VButtonGroup disabled variant="outline" tone="neutral" label="Row switched off">
      <VButton>Rename</VButton>
      <VButton>Duplicate</VButton>
      <VButton>Delete</VButton>
    </VButtonGroup>

    <VButtonGroup variant="outline" tone="neutral" label="One segment at a time">
      <VButton>Rename</VButton>
      <VButton disabled>Duplicate</VButton>
      <VButton loading>Deleting</VButton>
    </VButtonGroup>
  </div>
</template>

<style scoped>
.column {
  display: flex;
  flex-direction: column;
  align-items: start;
  gap: var(--vectis-space-3);
}
</style>

API

Props

PropTypeDefault
orientationButtonGroupOrientation'horizontal' | 'vertical''horizontal'
The direction the buttons are joined in: a row by default, or a column under vertical.
detachedbooleanfalse
Leaves the buttons as separate ones, with a gap between them and each keeping its own corners, instead of joining them into a segmented control. Everything the group hands down still travels, and an elevated row then lets each button cast its own shadow.
seamlessbooleanfalse
Takes the lines out from between the joined buttons: no seam is drawn, and the borders on both sides of every shared edge are cleared, so the row reads as one frame rather than as segments. The outer edges stay. It has no effect under detached, where there is no shared edge to take a line off.
fullWidthbooleanfalse
Stretches the row across the whole inline size of its parent, every segment taking an equal share of that width whatever its label measures. A segment never shrinks below its own label, so a row of labels too long for the parent overflows rather than being crushed. Under vertical it is the width alone, a column already stretching every segment across it.
variantButtonVariant'solid' | 'outline' | 'ghost' | 'soft'none
How much visual weight every segment carries, on VButton's own values: solid, outline, ghost or soft. It wins over the variant a button inside was given, a segment of another silhouette no longer reading as part of the row. Left out, each button keeps its own.
toneButtonTone'accent' | 'neutral' | 'danger'none
The colour the segments take, among accent, neutral and danger. This one is a fallback rather than an order: a button that names a tone of its own keeps it, which is what lets a single destructive action stand out in the row.
sizeButtonSize'xs' | 'sm' | 'md' | 'lg' | 'xl'none
The height of the segments, from the size scale shared by every control: xs, sm, md, lg or xl. It wins over the size a button inside was given. Left out, each button keeps its own.
compactbooleannone
Takes 4px off the height of every segment. It wins over the value a button inside was given. Left out, each button keeps its own.
elevatedbooleannone
Raises the row off the page with the shadow scale, on the terms of VButton's own prop. The shadow is the row's and not each segment's, which is what keeps the joints clear: three overlapping shadows would draw a dark band down each of them. It wins over the value a button inside was given. Left out, each button keeps its own.
disabledbooleannone
Makes every segment unusable. This one adds up rather than overruling: a button that disables itself stays disabled in a row that says nothing, and a segment cannot opt back in once the row is switched off.
labelstringnone
What screen readers announce for the row, which is a role="group": "Text formatting", "View". A group with no name is announced as a bare group. A consumer aria-label or aria-labelledby wins over it.

Slots

SlotType
default{}
The VButtons and VIconButtons to join together.