GrIcon
Renders an icon as a standalone interface element.
Machine-translated from the Russian original, not yet reviewed. Read the original
When to take it
- the icon needs a uniform size —
--gr-icon-sizeinstead of a width and a height in every place; - the icon takes its colour from the palette —
toneinstead of afillof your own; - the icon is meaningful —
labelgives it a name; without one it is hidden from a screen reader, and that is right by default; - the icon rotates —
spinfor a waiting state with no animation of your own.
When to take something else
| Need | Take |
|---|---|
| An icon inside a button | GrButton with square |
| The avatar of a person | GrAvatar |
| A key on the keyboard | GrKbd |
| A waiting indicator | GrLoading |
The component does not draw the icon itself — it arrives through a slot from any set
(unplugin-icons, an SVG of your own, a sprite). The package does not ship an icon set of
its own and will not: duplicating a delivery is harmful.
Decorative and meaningful
By default an icon is decorative: the component sets aria-hidden="true" itself. In the
library an icon almost always accompanies text, and the attribute used to be written by hand
at every call site — forget it once and a screen reader reads the <title> from the SVG.
label makes an icon meaningful: role="img" and aria-label appear, and the hiding is
removed.
<!-- the text "Saved" is next to it — the icon is decorative -->
<GrIcon>
<IconCheck />
</GrIcon>
<!-- the icon carries the meaning itself -->
<GrIcon label="Verified">
<IconCheck />
</GrIcon>
The rare case of “the icon is meaningful, but the name is given by a neighbouring element” is expressed with an ordinary attribute: a fallthrough is stronger than the component’s own binding.
The size
size is the scale of the package (xs | sm | md | lg, md by default) or an arbitrary
number in pixels. The scale is read from GrConfigProvider, and a number is a local escape
hatch past the config.
The values of the scale live as the --gr-icon-size-xs…lg tokens, so the size of the icons
is configured by the theme rather than by rebuilding the package. The --gr-icon-size
variable itself is a point of customisation in place: both the wrapper and the nested SVG
compute themselves from it.
The tone and the rotation
tone colours the icon with a text token (current by default — the colour is inherited
from the parent). A saturated tone as a text colour is forbidden in the package: on a light
background it gives a contrast of about 2:1, so the -text roles are used, as in GrAlert.
spin switches the rotation on for spinners. The component needs no separate support for
prefers-reduced-motion: the global clamp in base.css holds the animations, and the
rotation freezes in its starting position rather than at a random angle.
The style is deliberately global
The .gr-icon rule is not scoped: the SVG arrives through the consumer’s slot, and a scoped
style does not reach it — a :deep would have to be written at every call site.
Playground 2
Loading…
<GrIcon />Install
npm i @feugene/granularityImport
import { GrIcon } from '@feugene/granularity/components/GrIcon'API
Props
| Prop | Type | default | Description |
|---|---|---|---|
tone | GrIconTone | undefined | "current" | A colour from the palette. `current` — inherit the text colour of the parent. |
size | number | "xs" | "sm" | "md" | "lg" | undefined | undefined | The size by the scale of the package or an arbitrary one in pixels. |
label | string | undefined | undefined | The name of a meaningful icon. If it is set, the icon is declared `role="img"` and stops being hidden. Unset — the icon is decorative. |
spin | boolean | undefined | false | Rotation — for spinners. |
Slots
| Slot | Type | Description |
|---|---|---|
default | any | Markup of your own for the icon instead of a name from a set. |
Examples 4
Size scale
<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>Inline copy and link helpers
<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>Status cards and KPI tiles
<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>Decorative vs meaningful, tone and spin
<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>