GrIcon
Берут, когда иконке нужен единый размер.
Когда брать
- иконке нужен единый размер —
--gr-icon-sizeвместо ширины и высоты в каждом месте; - иконка берёт цвет из палитры —
toneвместо своегоfill; - иконка значима —
labelдаёт ей имя; без него она скрыта от скринридера, и это верно по умолчанию; - иконка вращается —
spinдля состояния ожидания без своей анимации.
Когда взять другое
| Нужно | Берите |
|---|---|
| Иконка внутри кнопки | GrButton с square |
| Аватар человека | GrAvatar |
| Клавиша на клавиатуре | GrKbd |
| Индикатор ожидания | GrLoading |
Саму иконку компонент не рисует — она приходит слотом из любого набора
(unplugin-icons, свой SVG, спрайт). Своего набора иконок пакет не поставляет
и не будет: дублировать поставку вредно.
Декоративная и значимая
По умолчанию иконка декоративна: компонент сам ставит aria-hidden="true".
В библиотеке иконка почти всегда сопровождает текст, и раньше атрибут писали
руками в каждом месте вызова — достаточно забыть один раз, и диктор прочитает
<title> из SVG.
Значимой иконку делает label: появляются role="img" и aria-label, а
скрытие снимается.
<!-- рядом есть текст «Сохранено» — иконка декоративна -->
<GrIcon>
<IconCheck />
</GrIcon>
<!-- иконка сама несёт смысл -->
<GrIcon label="Проверено">
<IconCheck />
</GrIcon>
Редкий случай «иконка значима, но имя даёт соседний элемент» выражается обычным атрибутом: fallthrough сильнее собственной привязки компонента.
Размер
size — шкала пакета (xs | sm | md | lg, по умолчанию md) или произвольное
число в пикселях. Шкала читается из GrConfigProvider, число — локальный
escape-hatch мимо конфига.
Значения шкалы живут токенами --gr-icon-size-xs…lg, поэтому размер иконок
настраивается темой, а не пересборкой пакета. Сама переменная
--gr-icon-size — точка кастомизации на месте: и обёртка, и вложенный SVG
считают себя от неё.
Тон и вращение
tone красит иконку токеном текста (current по умолчанию — цвет
наследуется от родителя). Насыщенный тон как цвет текста в пакете запрещён:
на светлой подложке он даёт контраст около 2:1, поэтому используются -text
роли, как в GrAlert.
spin включает вращение для спиннеров. Отдельной поддержки
prefers-reduced-motion компоненту не нужно: глобальный кламп в base.css
зажимает анимации, и вращение замирает в исходном положении, а не под
случайным углом.
Стиль намеренно глобальный
Правило .gr-icon не scoped: SVG приходит слотом потребителя, и scoped-стиль
до него не достаёт — пришлось бы писать :deep в каждом месте вызова.
Playground 2
Загружается…
<GrIcon />Установка
npm i @feugene/granularityИмпорт
import { GrIcon } from '@feugene/granularity/components/GrIcon'API
Props
| Prop | Type | по умолчанию | Описание |
|---|---|---|---|
tone | GrIconTone | undefined | "current" | Цвет из палитры. `current` — наследовать цвет текста родителя. |
size | number | "xs" | "sm" | "md" | "lg" | undefined | undefined | Размер по шкале пакета либо произвольный в пикселях. |
label | string | undefined | undefined | Имя значимой иконки. Задан — иконка объявляется `role="img"` и перестаёт быть скрытой. Не задан — иконка декоративна. |
spin | boolean | undefined | false | Вращение — для спиннеров. |
Slots
| Slot | Type | Описание |
|---|---|---|
default | any | Своя разметка иконки вместо имени из набора. |
Примеры 4
Шкала размеров
На странице важно показать, как GrIcon ведёт себя на разных размерах и почему он удобен как sizing-wrapper вокруг inline svg.
<script setup lang="ts">
import { GrIcon } from '@feugene/granularity'
const sizes = [12, 16, 20, 28, 36]
</script>
<template>
<div class="flex flex-wrap items-end gap-5">
<div
v-for="size in sizes"
:key="size"
class="flex flex-col items-center gap-2 text-xs text-[var(--gr-muted-fg)]"
>
<GrIcon :size="size">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" class="h-full w-full">
<path d="M12 3 14.8 8.7 21 9.6 16.5 14l1 6.1L12 17.1 6.5 20.1l1-6.1L3 9.6l6.2-.9L12 3Z" />
</svg>
</GrIcon>
<span>{{ size }}px</span>
</div>
</div>
</template>Иконка в строке текста и рядом со ссылкой
Показываем, что GrIcon можно встраивать в copy blocks, helper rows и рядом с GrLink, не ломая baseline текста.
<script setup lang="ts">
import { GrIcon, GrLink } from '@feugene/granularity'
const items = [
'Sync billing status every 5 minutes',
'Re-run failed payout webhooks automatically',
'Publish audit log snapshots to support inbox',
]
</script>
<template>
<div class="grid gap-3">
<div
v-for="item in items"
:key="item"
class="flex items-start gap-3 rounded-xl border border-[var(--gr-brd)] bg-[var(--gr-bg)] p-4"
>
<GrIcon size="md" class="mt-0.5 text-[var(--gr-primary-text)]">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" class="h-full w-full">
<path d="M4 12h16" />
<path d="m12 5 7 7-7 7" />
</svg>
</GrIcon>
<div class="min-w-0 text-sm text-[var(--gr-fg)]">
{{ item }}
</div>
</div>
<GrLink href="https://example.com" external size="md">
Explore icon usage inside inline content
</GrLink>
</div>
</template>Карточки статуса и плитки показателей
Отдельный сценарий для dashboards: GrIcon помогает собирать status cards и KPI summaries с предсказуемым tone/size contract.
<script setup lang="ts">
import { GrBadge, GrCard, GrIcon } from '@feugene/granularity'
import IconCircleCheck from '~icons/lucide/circle-check-big'
const stats = [
{ label: 'Healthy integrations', value: '18', tone: 'text-emerald-500' },
{ label: 'Needs review', value: '4', tone: 'text-amber-500' },
{ label: 'Failed checks', value: '1', tone: 'text-rose-500' },
]
</script>
<template>
<div class="grid gap-3 sm:grid-cols-3">
<GrCard
v-for="stat in stats"
:key="stat.label"
class="grid gap-3 p-4"
>
<div class="flex items-center justify-between gap-3">
<GrIcon size="lg" :class="stat.tone">
<IconCircleCheck />
</GrIcon>
<GrBadge size="sm" tone="neutral">
snapshot
</GrBadge>
</div>
<div>
<div class="text-2xl font-700 text-[var(--gr-fg)]">
{{ stat.value }}
</div>
<div class="text-sm text-[var(--gr-muted-fg)]">
{{ stat.label }}
</div>
</div>
</GrCard>
</div>
</template>Декоративная и значимая, тон и вращение
Иконка декоративна по умолчанию — компонент сам ставит aria-hidden. Значимой её делает label: появляются role="img" и имя. tone красит токеном текста (насыщенный тон как цвет текста в пакете запрещён — контраст падает до 2:1), spin крутит спиннер и сам замирает при prefers-reduced-motion.
<script setup lang="ts">
import { GrIcon, GR_TONES } from '@feugene/granularity'
</script>
<template>
<div class="grid gap-5">
<div class="grid gap-2">
<div class="text-xs text-[var(--gr-muted-fg)]">
Декоративная и значимая иконка
</div>
<div class="flex flex-wrap items-center gap-5 text-sm">
<!-- Рядом есть текст — иконка декоративна, компонент скрывает её сам. -->
<span class="inline-flex items-center gap-2">
<GrIcon>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8">
<path d="m5 13 4 4L19 7" />
</svg>
</GrIcon>
Сохранено
</span>
<!-- Текста рядом нет: смысл несёт сама иконка, значит ей нужно имя. -->
<GrIcon label="Проверено" tone="success">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8">
<path d="m5 13 4 4L19 7" />
</svg>
</GrIcon>
</div>
</div>
<div class="grid gap-2">
<div class="text-xs text-[var(--gr-muted-fg)]">
Тон из палитры (`-text`-роли токенов)
</div>
<div class="flex flex-wrap items-center gap-4">
<GrIcon v-for="tone in GR_TONES" :key="tone" :tone="tone" size="lg" :label="tone">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8">
<circle cx="12" cy="12" r="9" />
<path d="M12 8v5" />
<path d="M12 16h.01" />
</svg>
</GrIcon>
</div>
</div>
<div class="grid gap-2">
<div class="text-xs text-[var(--gr-muted-fg)]">
Вращение
</div>
<span class="inline-flex items-center gap-2 text-sm">
<GrIcon spin tone="primary">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8">
<path d="M21 12a9 9 0 1 1-6.2-8.6" />
</svg>
</GrIcon>
Загружаем…
</span>
</div>
</div>
</template>