GrButtonGroup

Пакет: @feugene/granularityядроГруппа: Действия

Берут, когда действия одного класса стоят рядом.

Когда брать

  • действия одного класса стоят рядом — панель инструментов, ряд «Сохранить / Отмена», переключатели вида;
  • ряд должен читаться как одно целое — внутренние границы не задваиваются, скругления остаются по краям;
  • оформление задаётся один раз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

PropTypeпо умолчаниюОписание
tone"primary" | "neutral" | "success" | "warning" | "danger" | "info" | "slate" | "azure" | undefinedundefined
variantGrButtonVariant | undefinedundefined
size"xs" | "sm" | "md" | "lg" | undefinedundefinedОформление, общее для кнопок группы. Проп самой кнопки сильнее.
ariaLabelstring | undefinedundefinedДоступное имя группы: без него кнопки читаются как несвязанные.
orientationGrButtonGroupOrientation | undefined"horizontal"
attachedboolean | undefinedtrueСклеивать кнопки в один блок. `false` — обычный ряд с зазором: каждая кнопка сохраняет свои радиусы и границы.

Slots

SlotTypeОписание
defaultanyКнопки группы.

Примеры 5

Переключатель вида сегментами

Базовый composition-сценарий: GrButtonGroup собирает несколько adjacent actions в единый segmented control для view-mode и similar state switches.

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>

Компактный кластер панели инструментов

Показываем GrButtonGroup как контейнер для плотной action-toolbar, где важна визуальная связность соседних кнопок.

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>

Общее оформление и кнопки в обёртках

Размер, вариант и тон задаются один раз на группе и доходят до кнопок; проп самой кнопки сильнее группы, а группа сильнее GrConfigProvider — она ближе. Обёртка вокруг кнопки (тултип, v-if-спан, роутерная ссылка) ряд не разрывает: склейка считает звенья группы, а не прямых потомков.

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>

Вертикальная группа и режим без склейки

orientation="vertical" собирает кнопки в столбец — скругления переезжают на верхний и нижний края. :attached="false" даёт обычный ряд с зазором: каждая кнопка сохраняет свои радиусы и границы.

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>

Композиция панели фильтров

Группа подходит и для shallow filters: рядом с cards/list states можно быстро переключать сегменты без отдельного tabs-компонента.

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>

Документация компонентаВсе компоненты