GrEmptyState
Explains an empty state and suggests the user’s next step.
Machine-translated from the Russian original, not yet reviewed. Read the original
When to take it
- the list is empty from the very beginning — “no payouts yet”: an explanation and a next step instead of an empty rectangle;
- the filter found nothing — the text and the action are different here: reset the filter, not create the first record;
- the section has not been set up yet — an icon, a heading, a description and a button instead of an instruction from support;
- the state is built into another component — lists, tables and charts show it through the
#emptyslot.
When to take something else
| Need | Take |
|---|---|
| The data is loading | GrSkeleton / GrLoading |
| The request failed | GrResponseErrorBanner |
| A warning in the flow of the page | GrAlert |
| Just text with no illustration is needed | GrCard |
An empty screen without an explanation reads as a breakage. The difference between “there is nothing here yet” and “nothing was found for this filter” matters: in the first case you are offered to create something, in the second to remove the filter.
The heading
<GrEmptyState title="No payouts yet" description="Create the first payout to track approvals.">
<GrButton size="sm">Create a payout</GrButton>
</GrEmptyState>
The heading is a real heading: without it an empty state is not found by navigating
across headings, and it is exactly the empty state that explains what is happening on the
screen. The level is set by headingLevel (2…6, 3 by default) and configured
globally through componentDefaults.GrEmptyState.headingLevel. There is no h1 in the
set on purpose: an empty state lives inside an already existing structure of the page
rather than setting it.
title is optional: without it and without the slot a string from the locale is taken
(gr.emptyState.title), so that <GrEmptyState /> does not turn into an empty card.
The slots
| Slot | What for |
|---|---|
title | the heading as markup: a link, an emphasis, a counter |
description | the description as markup |
icon | an icon of your own instead of the built-in lucide/inbox |
| the default one | the actions under the text, centred |
A slot is stronger than the prop of the same name. The built-in icon is decorative
(aria-hidden) — the meaning is carried by the text.
The surface
<GrCard>
<GrEmptyState variant="ghost" title="Nobody has been invited" />
</GrCard>
variant (outlined by default, ghost) repeats the vocabulary of
GrCard: ghost removes the border, the background and the radius. It is
needed inside an already existing card — in GrDataTable #empty, in GrList #empty or in
a layout of your own, where outlined gives a second border around the same surface.
The size
size (xs…lg, md by default, read from GrConfigProvider) moves the padding of the
card, the box of the icon, the icon itself, the type size of the heading and the vertical
spacing. xs and sm are for an empty table cell and a dropdown panel, where a
full-size state takes up the whole list.
Styling
Everything comes from tokens: the radius from --gr-radius-md/--gr-radius-lg by step,
the type sizes from --gr-text-xs/--gr-text-sm/--gr-text-base, the colours from
--gr-card, --gr-brd, --gr-muted, --gr-muted-fg.
Playground 4
Loading…
<GrEmptyState />Install
npm i @feugene/granularityImport
import { GrEmptyState } from '@feugene/granularity/components/GrEmptyState'API
Props
| Prop | Type | default | Description |
|---|---|---|---|
variant | "ghost" | "outlined" | undefined | undefined | `ghost` removes the border and the background: a card inside a card draws a second border. |
title | string | undefined | undefined | The heading. Unset — it comes from the locale; the `#title` slot is stronger than both. |
size | "xs" | "sm" | "md" | "lg" | undefined | undefined | — |
description | string | undefined | undefined | — |
headingLevel | 2 | 3 | 4 | 5 | 6 | undefined | undefined | — |
Slots
| Slot | Type | Description |
|---|---|---|
icon | any | An icon instead of the built-in one. |
title | any | The heading as markup: a link, an emphasis, a counter. |
description | any | — |
default | any | The actions under the text: they are centred. |
Examples 3
Primary CTA inside card surface
No payouts yet
<script setup lang="ts">
import { GrButton, GrEmptyState } from '@feugene/granularity'
</script>
<template>
<GrEmptyState title="No payouts yet" description="Create the first scheduled payout to start tracking approval and transfer states.">
<GrButton size="sm">Create payout</GrButton>
</GrEmptyState>
</template>Search/filter zero-results flow
Nothing found
<script setup lang="ts">
import { computed, ref } from 'vue'
import { GrButton, GrEmptyState, GrInput } from '@feugene/granularity'
const query = ref('treasury')
const description = computed(() => {
return `No saved views match “${query.value}”. Try a broader term or create a new filtered workspace.`
})
</script>
<template>
<div class="grid gap-3">
<GrInput v-model="query" placeholder="Search views" />
<GrEmptyState size="sm" title="Nothing found" :description="description">
<div class="flex flex-wrap justify-center gap-2">
<GrButton size="sm" variant="outline">Clear filter</GrButton>
<GrButton size="sm">Create view</GrButton>
</div>
</GrEmptyState>
</div>
</template>Embedded inside split layout
No team members invited
<script setup lang="ts">
import { GrButton, GrCard, GrEmptyState } from '@feugene/granularity'
</script>
<template>
<GrCard class="grid gap-4 md:grid-cols-[minmax(0,220px)_1fr] md:items-center">
<div class="rounded-xl border border-dashed border-[var(--gr-brd)] bg-[var(--gr-muted)] p-4 text-sm text-[var(--gr-muted-fg)]">
Left rail can keep filters, contextual hints or a compact KPI while the main area uses `GrEmptyState`.
</div>
<GrEmptyState variant="ghost" title="No team members invited" description="Inside an existing card the ghost variant drops the second border around the same surface.">
<GrButton size="sm">Invite teammate</GrButton>
</GrEmptyState>
</GrCard>
</template>