GrButtonGroup
Берут, когда действия одного класса стоят рядом.
Когда брать
- действия одного класса стоят рядом — панель инструментов, ряд «Сохранить / Отмена», переключатели вида;
- ряд должен читаться как одно целое — внутренние границы не задваиваются, скругления остаются по краям;
- оформление задаётся один раз —
size,toneиvariantраздаются всем кнопкам разом; - группе нужно имя —
ariaLabelобъясняет, что объединяет эти кнопки.
Когда взять другое
| Нужно | Берите |
|---|---|
| Выбирается значение, а не выполняется действие | GrSegmented |
| Действий много и они прячутся | GrDropdownMenu |
| Действие одно | GrButton |
| Разделы с разным содержимым | GrTabs |
Группа кнопок и сегменты выглядят похоже и означают разное: здесь каждая кнопка делает что-то и после нажатия ничего не остаётся выбранным, там один из вариантов всегда активен.
Оформление одной строкой
<GrButtonGroup aria-label="Период" size="sm" variant="outline" tone="neutral">
<GrButton>День</GrButton>
<GrButton>Неделя</GrButton>
<GrButton tone="primary">Месяц</GrButton>
</GrButtonGroup>
size, variant и tone группы доходят до кнопок через контекст. Порядок
разрешения: проп кнопки → группа → GrConfigProvider → дефолт — группа
ближе к кнопке, чем глобальный провайдер, поэтому побеждает его, но не
собственный проп кнопки.
Контекст доступен и снаружи — useGrButtonGroup(), если поверх группы строится
свой контрол.
Обёртки не разрывают ряд
Склейка считает звенья группы — прямых потомков, которые сами кнопка или
содержат кнопку. Поэтому кнопку можно завернуть в тултип, v-if-обёртку или
роутерную ссылку, и ряд останется целым:
<GrButtonGroup aria-label="Документ">
<GrButton>Открыть</GrButton>
<GrTooltip content="Копия появится рядом">
<GrButton>Дублировать</GrButton>
</GrTooltip>
</GrButtonGroup>
Не-кнопочный потомок (разделитель, подпись) в склейку не входит и скругление себе не забирает.
Ориентация и режим без склейки
| Проп | Что делает |
|---|---|
orientation="vertical" | столбец: скругления переезжают на верхний и нижний края ряда |
:attached="false" | обычный ряд с зазором, каждая кнопка со своими радиусами |
Радиус
Группа берёт радиус из той же точки кастомизации, что и сама кнопка, —
--gr-button-radius (по умолчанию 0.375rem). Задайте её один раз, и края
группы поедут вместе с кнопками:
.app { --gr-button-radius: 12px; }Доступность
Корень объявлен role="group"; имя задаётся пропом ariaLabel — без него набор
кнопок читается как несвязанный. Кнопка под курсором или с фокусом поднимается
над соседями, поэтому фокус-кольцо не срезается наложением границ.
Playground 4
Загружается…
<GrButtonGroup />Установка
npm i @feugene/granularityИмпорт
import { GrButtonGroup } from '@feugene/granularity/components/GrButtonGroup'API
Props
| Prop | Type | по умолчанию | Описание |
|---|---|---|---|
tone | "primary" | "neutral" | "success" | "warning" | "danger" | "info" | "slate" | "azure" | undefined | undefined | — |
variant | GrButtonVariant | undefined | undefined | — |
size | "xs" | "sm" | "md" | "lg" | undefined | undefined | Оформление, общее для кнопок группы. Проп самой кнопки сильнее. |
ariaLabel | string | undefined | undefined | Доступное имя группы: без него кнопки читаются как несвязанные. |
orientation | GrButtonGroupOrientation | undefined | "horizontal" | — |
attached | boolean | undefined | true | Склеивать кнопки в один блок. `false` — обычный ряд с зазором: каждая кнопка сохраняет свои радиусы и границы. |
Slots
| Slot | Type | Описание |
|---|---|---|
default | any | Кнопки группы. |
Примеры 5
Переключатель вида сегментами
Базовый composition-сценарий: GrButtonGroup собирает несколько adjacent actions в единый segmented control для view-mode и similar state switches.
<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>Компактный кластер панели инструментов
Показываем GrButtonGroup как контейнер для плотной action-toolbar, где важна визуальная связность соседних кнопок.
Release note title
Button groups удобно использовать в компактных toolbars, где важна предсказуемая ширина и визуальная связность соседних действий.
<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>Общее оформление и кнопки в обёртках
Размер, вариант и тон задаются один раз на группе и доходят до кнопок; проп самой кнопки сильнее группы, а группа сильнее GrConfigProvider — она ближе. Обёртка вокруг кнопки (тултип, v-if-спан, роутерная ссылка) ряд не разрывает: склейка считает звенья группы, а не прямых потомков.
<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>Вертикальная группа и режим без склейки
orientation="vertical" собирает кнопки в столбец — скругления переезжают на верхний и нижний края. :attached="false" даёт обычный ряд с зазором: каждая кнопка сохраняет свои радиусы и границы.
<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>Композиция панели фильтров
Группа подходит и для shallow filters: рядом с cards/list states можно быстро переключать сегменты без отдельного tabs-компонента.
<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>