GrIcon

Package: @feugene/granularitycoreGroup: data

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-size instead of a width and a height in every place;
  • the icon takes its colour from the palettetone instead of a fill of your own;
  • the icon is meaningfullabel gives it a name; without one it is hidden from a screen reader, and that is right by default;
  • the icon rotatesspin for a waiting state with no animation of your own.

When to take something else

NeedTake
An icon inside a buttonGrButton with square
The avatar of a personGrAvatar
A key on the keyboardGrKbd
A waiting indicatorGrLoading

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…

Code
<GrIcon />

Install

npm i @feugene/granularity

Import

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

API

Props

PropTypedefaultDescription
toneGrIconTone | undefined"current"A colour from the palette. `current` — inherit the text colour of the parent.
sizenumber | "xs" | "sm" | "md" | "lg" | undefinedundefinedThe size by the scale of the package or an arbitrary one in pixels.
labelstring | undefinedundefinedThe name of a meaningful icon. If it is set, the icon is declared `role="img"` and stops being hidden. Unset — the icon is decorative.
spinboolean | undefinedfalseRotation — for spinners.

Slots

SlotTypeDescription
defaultanyMarkup of your own for the icon instead of a name from a set.

Examples 4

Size scale

12px
16px
20px
28px
36px

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

Sync billing status every 5 minutes
Re-run failed payout webhooks automatically
Publish audit log snapshots to support inbox
Explore icon usage inside inline content (opens in a new tab)

Inline Copy
<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

Status Carddepends on the showcase environment
<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

Декоративная и значимая иконка
Сохранено
Тон из палитры (`-text`-роли токенов)
Вращение
Загружаем…

Semantics
<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>

Component documentationAll components