GrButtonGroup

Package: @feugene/granularitycoreGroup: actions

Groups related buttons into a compact action cluster.

Machine-translated from the Russian original, not yet reviewed. Read the original

When to take it

  • actions of the same class stand side by side — a toolbar, a “Save / Cancel” row, view switches;
  • the row has to read as one whole — the inner borders are not doubled, the rounding stays at the edges;
  • the styling is set oncesize, tone and variant are handed to every button at once;
  • the group needs a nameariaLabel explains what unites these buttons.

When to take something else

NeedTake
A value is selected rather than an action performedGrSegmented
There are many actions and they are hiddenGrDropdownMenu
There is a single actionGrButton
Sections with different contentGrTabs

A button group and a segmented control look alike and mean different things: here every button does something and nothing stays selected after the press, there one of the options is always active.

Styling in one line

<GrButtonGroup aria-label="Period" size="sm" variant="outline" tone="neutral">
  <GrButton>Day</GrButton>
  <GrButton>Week</GrButton>
  <GrButton tone="primary">Month</GrButton>
</GrButtonGroup>

The size, variant and tone of the group reach the buttons through context. The resolution order: the button’s prop → the group → GrConfigProvider → the default — the group is closer to the button than the global provider, so it beats the provider, but not the button’s own prop.

The context is available from the outside as well — useGrButtonGroup(), if a control of your own is built on top of the group.

Wrappers do not break the row

The gluing counts the links of the group — direct children that either are a button or contain one. A button can therefore be wrapped into a tooltip, a v-if wrapper or a router link, and the row stays whole:

<GrButtonGroup aria-label="Document">
  <GrButton>Open</GrButton>
  <GrTooltip content="The copy will appear next to it">
    <GrButton>Duplicate</GrButton>
  </GrTooltip>
</GrButtonGroup>

A non-button child (a separator, a label) does not enter the gluing and does not take any rounding for itself.

Orientation and the mode without gluing

PropWhat it does
orientation="vertical"a column: the rounding moves to the top and bottom edges of the row
:attached="false"an ordinary row with a gap, every button with its own radii

The radius

The group takes its radius from the same point of customisation as the button itself — --gr-button-radius (0.375rem by default). Set it once and the edges of the group move together with the buttons:

.app { --gr-button-radius: 12px; }

Accessibility

The root is declared role="group"; the name is set with the ariaLabel prop — without it a set of buttons reads as unrelated. The button under the cursor or with focus rises above its neighbours, so the focus ring is not clipped by the overlapping borders.

Playground 4

Loading…

Code
<GrButtonGroup />

Install

npm i @feugene/granularity

Import

import { GrButtonGroup } from '@feugene/granularity/components/GrButtonGroup'

API

Props

PropTypedefaultDescription
tone"primary" | "neutral" | "success" | "warning" | "danger" | "info" | "slate" | "azure" | undefinedundefined
variantGrButtonVariant | undefinedundefined
size"xs" | "sm" | "md" | "lg" | undefinedundefinedStyling shared by the buttons of the group. The prop of a button itself is stronger.
ariaLabelstring | undefinedundefinedThe accessible name of the group: without it the buttons read as unrelated.
orientationGrButtonGroupOrientation | undefined"horizontal"
attachedboolean | undefinedtrueGlue the buttons into one block. `false` — an ordinary row with a gap: every button keeps its own radii and borders.

Slots

SlotTypeDescription
defaultanyThe buttons of the group.

Examples 5

Segmented view switcher

Active: board
Используйте группу, когда несколько action-кнопок переключают один контекст и должны восприниматься как единый control cluster.

Segmented
<script setup lang="ts">
import { ref } from 'vue'

import { GrBadge, GrButton, GrButtonGroup } from '@feugene/granularity'

const currentView = ref('board')

const views = [
  { label: 'Board', value: 'board' },
  { label: 'List', value: 'list' },
  { label: 'Calendar', value: 'calendar' },
]
</script>

<template>
  <div class="grid gap-4">
    <div class="flex flex-wrap items-center justify-between gap-3">
      <GrButtonGroup aria-label="View switcher">
        <GrButton
          v-for="view in views"
          :key="view.value"
          size="sm"
          :variant="currentView === view.value ? 'primary' : 'outline'"
          @click="currentView = view.value"
        >
          {{ view.label }}
        </GrButton>
      </GrButtonGroup>

      <GrBadge size="sm" tone="primary">
        Active: {{ currentView }}
      </GrBadge>
    </div>

    <div class="rounded-xl border border-[var(--gr-brd)] bg-[var(--gr-bg)] p-4 text-sm text-[var(--gr-muted-fg)]">
      Используйте группу, когда несколько action-кнопок переключают один контекст и должны восприниматься как единый control cluster.
    </div>
  </div>
</template>

Compact toolbar cluster

boldunderline

Release note title

Button groups удобно использовать в компактных toolbars, где важна предсказуемая ширина и визуальная связность соседних действий.

Toolbar
<script setup lang="ts">
import { ref } from 'vue'

import { GrBadge, GrButton, GrButtonGroup, GrCard } from '@feugene/granularity'

const activeTools = ref(['bold', 'underline'])

const tools = [
  { label: 'B', value: 'bold' },
  { label: 'I', value: 'italic' },
  { label: 'U', value: 'underline' },
]

function toggleTool(tool: string) {
  if (activeTools.value.includes(tool)) {
    activeTools.value = activeTools.value.filter(value => value !== tool)
    return
  }

  activeTools.value = [...activeTools.value, tool]
}
</script>

<template>
  <GrCard class="grid gap-4 p-4">
    <div class="flex flex-wrap items-center justify-between gap-3">
      <GrButtonGroup aria-label="Formatting toolbar">
        <GrButton
          v-for="tool in tools"
          :key="tool.value"
          size="sm"
          :variant="activeTools.includes(tool.value) ? 'primary' : 'outline'"
          @click="toggleTool(tool.value)"
        >
          {{ tool.label }}
        </GrButton>
      </GrButtonGroup>

      <div class="flex flex-wrap gap-2">
        <GrBadge
          v-for="tool in activeTools"
          :key="tool"
          size="sm"
          tone="neutral"
        >
          {{ tool }}
        </GrBadge>
      </div>
    </div>

    <p class="text-sm text-[var(--gr-fg)]">
      Release note title
    </p>
    <p class="text-sm text-[var(--gr-muted-fg)]">
      Button groups удобно использовать в компактных toolbars, где важна предсказуемая ширина и визуальная связность соседних действий.
    </p>
  </GrCard>
</template>

Shared styling and wrapped buttons

Shared Style
<script setup lang="ts">
import { GrButton, GrButtonGroup, GrTooltip } from '@feugene/granularity'
</script>

<template>
  <div class="grid gap-4">
    <!-- Оформление задаётся один раз на группе, а не повторяется на каждой кнопке. -->
    <GrButtonGroup aria-label="Период отчёта" size="sm" variant="outline" tone="neutral">
      <GrButton>День</GrButton>
      <GrButton>Неделя</GrButton>
      <GrButton tone="primary">
        Месяц
      </GrButton>
    </GrButtonGroup>

    <!-- Обёртка вокруг кнопки не разрывает ряд: склейка считает звенья, а не прямых потомков. -->
    <GrButtonGroup aria-label="Действия над документом" variant="outline">
      <GrButton>Открыть</GrButton>
      <GrTooltip text="Копия появится рядом с оригиналом">
        <GrButton>Дублировать</GrButton>
      </GrTooltip>
      <GrButton>Архивировать</GrButton>
    </GrButtonGroup>
  </div>
</template>

Vertical group and spaced mode

Orientation
<script setup lang="ts">
import { GrButton, GrButtonGroup } from '@feugene/granularity'
</script>

<template>
  <div class="flex flex-wrap items-start gap-8">
    <GrButtonGroup aria-label="Слои карты" orientation="vertical" variant="outline">
      <GrButton>Схема</GrButton>
      <GrButton>Спутник</GrButton>
      <GrButton>Гибрид</GrButton>
    </GrButtonGroup>

    <!-- `attached: false` — тот же ряд, но без склейки: каждая кнопка со своими радиусами. -->
    <GrButtonGroup aria-label="Экспорт" :attached="false" variant="ghost">
      <GrButton>CSV</GrButton>
      <GrButton>XLSX</GrButton>
      <GrButton>PDF</GrButton>
    </GrButtonGroup>
  </div>
</template>

Filter rail composition

All
Queue segment
24
Drafts
Queue segment
6
Scheduled
Queue segment
8
Failed
Queue segment
2

Filter Rail
<script setup lang="ts">
import { ref } from 'vue'

import { GrBadge, GrButton, GrButtonGroup, GrCard } from '@feugene/granularity'

const currentFilter = ref('all')

const filters = [
  { label: 'All', value: 'all', count: 24 },
  { label: 'Drafts', value: 'drafts', count: 6 },
  { label: 'Scheduled', value: 'scheduled', count: 8 },
  { label: 'Failed', value: 'failed', count: 2 },
]
</script>

<template>
  <div class="grid gap-4">
    <GrButtonGroup aria-label="Content filters">
      <GrButton
        v-for="filter in filters"
        :key="filter.value"
        size="sm"
        :variant="currentFilter === filter.value ? 'primary' : 'outline'"
        @click="currentFilter = filter.value"
      >
        {{ filter.label }}
      </GrButton>
    </GrButtonGroup>

    <div class="grid gap-3 sm:grid-cols-2 xl:grid-cols-4">
      <GrCard
        v-for="filter in filters"
        :key="filter.value"
        class="flex items-center justify-between gap-3 p-4"
      >
        <div>
          <div class="text-sm font-600 text-[var(--gr-fg)]">
            {{ filter.label }}
          </div>
          <div class="text-xs text-[var(--gr-muted-fg)]">
            Queue segment
          </div>
        </div>

        <GrBadge :tone="currentFilter === filter.value ? 'primary' : 'neutral'">
          {{ filter.count }}
        </GrBadge>
      </GrCard>
    </div>
  </div>
</template>

Component documentationAll components