GrSkeleton
A temporary interface skeleton while the content is still loading.
Machine-translated from the Russian original, not yet reviewed. Read the original
When to take it
- there is no content yet — the placeholder repeats its shape, and the layout does not jump when the data appears;
- the first render is loading — a card, a list, a table: the user sees the structure before the data;
- there are several placeholders —
countrepeats a row the required number of times; - the shape is non-standard —
variantgives a line, a block or a circle for an avatar.
When to take something else
| Need | Take |
|---|---|
| The content is already there but is being updated | GrLoading |
| The share of what is done is known | GrProgressBar |
| The loading has finished and there is no data | GrEmptyState |
| The wait is inside a button | GrButton with loading |
A skeleton is honest only when it repeats the future layout. Three identical lines instead of a table of six columns deceive twice: first they promise the wrong thing, then they give a jump of the layout.
The shape by variant, the sizes by the consumer
variant | The radius | What for |
|---|---|---|
text (the default) | --gr-radius-full | a line of text |
rect | --gr-radius-md | a block: a card, a button, a picture |
circle | --gr-radius-full | an avatar, an icon |
The variant sets the shape only. width and height stay with the consumer: the height of a
placeholder is dictated by the content it stands in for rather than by the scale of the component.
The default is 100% × 12px, and 2.5rem for a circle.
The circle is the only shape where the sizes are linked: height repeats width until it is set
explicitly. Otherwise one side that was set would turn the circle into an oval.
rounded overrides the radius of the variant pointwise — for instance when the card of an
application has a large radius of its own.
A block of several lines
<GrSkeleton :count="3" />
count draws N placeholders in a shared grid. In text the last line is shorter (60%): the
block reads as a paragraph rather than as a list of identical bars. In rect and circle all of
the elements are identical — they have no “unfinished line”. A width that has been set is
stronger than that rule.
A single placeholder (the default count) renders without a wrapper — with exactly the same
node as before.
Accessibility
The placeholder as a whole is marked aria-hidden="true" — both a single one and the wrapper of a
group. There is no point announcing an “empty” node; the fact that loading is going on is reported
by the container: aria-busy="true" and a live region with text. That is how it is done inside the
package — see the loading state of GrStatistic and GrTable.
The movement
The pulsing is a @keyframes of the component’s own, and under prefers-reduced-motion: reduce it
is switched off entirely (animation: none) rather than sped up: an endless animation is movement
the user has asked not to be shown. The whole contract of movement — ../motion.md.
The <style> of the component is deliberately not scoped: the rule hangs on the
[data-gr-skeleton] attribute, so it reaches placeholders rendered inside other components of the
package as well.
Playground 5
Loading…
<GrSkeleton />Install
npm i @feugene/granularityImport
import { GrSkeleton } from '@feugene/granularity/components/GrSkeleton'API
Props
| Prop | Type | default | Description |
|---|---|---|---|
variant | "circle" | "text" | "rect" | undefined | "text" | The shape rather than the dimensions: `width`/`height` stay with the consumer, because the height of a placeholder is dictated by the neighbouring content rather than by the scale of the component. |
width | string | undefined | undefined | — |
height | string | undefined | undefined | — |
rounded | string | undefined | undefined | The radius pointwise. Unset — it is taken from `variant`. |
count | number | undefined | 1 | How many placeholders in a row. More than one and they go into a common wrapper. |
Examples 4
Shape and repetition
<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>Text card placeholder
<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>Avatar/list row placeholders
<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>Dashboard and chart layout
<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>