GrSkeleton
Берут, когда контента ещё нет.
Когда брать
- контента ещё нет — заглушка повторяет его форму, и раскладка не прыгает при появлении данных;
- загрузка первой отрисовки — карточка, список, таблица: пользователь видит структуру до данных;
- заглушек несколько —
countповторяет строку нужное число раз; - форма нестандартная —
variantдаёт полосу, блок или круг под аватар.
Когда взять другое
| Нужно | Берите |
|---|---|
| Контент уже есть, но обновляется | GrLoading |
| Известна доля выполненного | GrProgressBar |
| Загрузка закончилась, данных нет | GrEmptyState |
| Ожидание внутри кнопки | GrButton с loading |
Скелетон честен только тогда, когда повторяет будущую раскладку. Три одинаковые полосы вместо таблицы из шести колонок обманывают дважды: сначала обещают не то, потом дают прыжок раскладки.
Форма — вариантом, размеры — потребителем
variant | Радиус | Для чего |
|---|---|---|
text (по умолчанию) | --gr-radius-full | строка текста |
rect | --gr-radius-md | блок: карточка, кнопка, картинка |
circle | --gr-radius-full | аватар, иконка |
Вариант задаёт только форму. width и height остаются на потребителе:
высоту заглушки диктует контент, который она подменяет, а не шкала компонента.
Дефолт — 100% × 12px, у круга — 2.5rem.
Круг — единственная форма, где размеры связаны: height повторяет width, пока
её не задали явно. Иначе одна заданная сторона превращала бы круг в овал.
rounded перекрывает радиус варианта точечно — например, когда у карточки
приложения свой крупный радиус.
Блок из нескольких строк
<GrSkeleton :count="3" />
count рисует N заглушек в общей сетке. У text последняя строка короче
(60%): блок читается как абзац, а не как список одинаковых полос. У rect и
circle все элементы одинаковые — «недописанной строки» у них не бывает.
Заданный width сильнее этого правила.
Одна заглушка (count по умолчанию) рендерится без обёртки — ровно тем же
узлом, что и раньше.
Доступность
Заглушка целиком помечена aria-hidden="true" — и одиночная, и обёртка группы.
Озвучивать «пустой» узел незачем; о том, что идёт загрузка, сообщает контейнер:
aria-busy="true" и живой регион с текстом. Так это сделано внутри пакета —
см. состояние loading у GrStatistic и GrTable.
Движение
Пульсация — собственный @keyframes компонента, и под
prefers-reduced-motion: reduce она выключается полностью (animation: none),
а не ускоряется: бесконечная анимация — это движение, которого пользователь
попросил не показывать. Контракт движения целиком — ../motion.md.
<style> у компонента без scoped намеренно: правило висит на атрибуте
[data-gr-skeleton], поэтому доезжает и до заглушек, отрисованных внутри других
компонентов пакета.
Playground 5
Загружается…
<GrSkeleton />Установка
npm i @feugene/granularityИмпорт
import { GrSkeleton } from '@feugene/granularity/components/GrSkeleton'API
Props
| Prop | Type | по умолчанию | Описание |
|---|---|---|---|
variant | "circle" | "text" | "rect" | undefined | "text" | Форма, а не размеры: `width`/`height` остаются на потребителе, потому что высота заглушки диктуется соседним контентом, а не шкалой компонента. |
width | string | undefined | undefined | — |
height | string | undefined | undefined | — |
rounded | string | undefined | undefined | Радиус точечно. Не задан — берётся из `variant`. |
count | number | undefined | 1 | Сколько заглушек подряд. Больше одной — они уезжают в общую обёртку. |
Примеры 4
Форма и повторение
variant задаёт форму, а не размеры: text — пилюля, rect — скруглённый блок, circle — круг (высота повторяет ширину). Размеры остаются на потребителе, потому что высоту заглушки диктует соседний контент. count рисует блок из N заглушек одним пропом; у text последняя строка короче, поэтому блок читается абзацем, а не списком одинаковых полос.
<script setup lang="ts">
import { GrSkeleton } from '@feugene/granularity'
</script>
<template>
<div class="grid gap-4">
<div class="grid gap-3 sm:grid-cols-3">
<div class="grid gap-2">
<span class="text-xs text-[var(--gr-muted-fg)]">variant="text"</span>
<GrSkeleton />
</div>
<div class="grid gap-2">
<span class="text-xs text-[var(--gr-muted-fg)]">variant="rect"</span>
<GrSkeleton variant="rect" height="40px" />
</div>
<div class="grid gap-2">
<span class="text-xs text-[var(--gr-muted-fg)]">variant="circle"</span>
<GrSkeleton variant="circle" width="40px" />
</div>
</div>
<div class="grid gap-2">
<span class="text-xs text-[var(--gr-muted-fg)]">:count="4"</span>
<GrSkeleton :count="4" />
</div>
</div>
</template>Одна заглушка рендерится без обёртки — DOM тех, кто уже использует GrSkeleton, от появления count не меняется. Заглушка целиком aria-hidden: загрузку объявляет контейнер (aria-busy + живой регион), а не полоса.
Заглушка текстовой карточки
Базовый loading-surface для статей, карточек и описательных блоков: заголовок variant="rect" и абзац одним :count="3" вместо трёх строк вручную.
<script setup lang="ts">
import { GrCard, GrSkeleton } from '@feugene/granularity'
</script>
<template>
<GrCard class="grid gap-4 p-4">
<GrSkeleton variant="rect" width="38%" height="20px" />
<!-- Три строки одним пропом: последняя короче, поэтому блок читается абзацем. -->
<GrSkeleton :count="3" />
</GrCard>
</template>Заглушки строки списка с аватаром
Data-display сценарий для таблиц и списков: avatar, две текстовые строки и trailing action area.
<script setup lang="ts">
import { GrSkeleton } from '@feugene/granularity'
const rows = [1, 2, 3]
</script>
<template>
<div class="grid gap-3 rounded-xl border border-[var(--gr-brd)] bg-[var(--gr-card)] p-4">
<div v-for="row in rows" :key="row" class="flex items-center gap-3">
<GrSkeleton variant="circle" width="44px" />
<div class="min-w-0 flex-1 grid gap-2">
<GrSkeleton width="44%" />
<GrSkeleton width="72%" />
</div>
<GrSkeleton variant="rect" width="72px" height="28px" />
</div>
</div>
</template>Раскладка дашборда и графика
Комбинируем разные размеры GrSkeleton, чтобы быстро собрать loading-layout для dashboard, chart и KPI blocks.
<script setup lang="ts">
import { GrSkeleton } from '@feugene/granularity'
</script>
<template>
<div class="grid gap-3 md:grid-cols-[minmax(0,1.4fr)_minmax(0,1fr)]">
<div class="grid gap-3 rounded-xl border border-[var(--gr-brd)] bg-[var(--gr-card)] p-4">
<GrSkeleton width="26%" height="18px" rounded="12px" />
<GrSkeleton height="160px" rounded="18px" />
<div class="grid gap-2 sm:grid-cols-3">
<GrSkeleton v-for="card in 3" :key="card" height="72px" rounded="16px" />
</div>
</div>
<div class="grid gap-3 rounded-xl border border-[var(--gr-brd)] bg-[var(--gr-card)] p-4">
<GrSkeleton width="48%" height="18px" rounded="12px" />
<GrSkeleton :count="4" width="100%" />
</div>
</div>
</template>