GrEmptyState
Explains an empty state and suggests the user’s next step.
Overview
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.
Variants every tone and view, rendered by the real component
variant
ghostoutlined
Sizes
xssmmdlg
Playground controls come from the API, not from hand
Loading…
0 changed· state is in the URL
<GrEmptyState title="No invoices yet" description="Create the first one, or bring last year's from a CSV."><GrButton size="sm">New invoice</GrButton></GrEmptyState>Examples 3 each with a live demo and its code
The first day of an invoice list
Where the rows will be, the empty state offers the actions that fill them: the default slot holds the buttons, and the description says what happens next rather than that the list is empty.
variant="ghost"because the list is already a frame; once an invoice exists, the list replaces the emptiness.InvoicesNo invoices yet
Create the first one, or bring last year's from a CSV. Clients get each invoice by email with a payment link.Nothing found is not nothing yet
kind="search"draws a magnifier and names another kind of emptiness. Thetitleslot quotes the query so the typo shows, and the actions fix the query or widen the search to archived customers. Offering to create a customer here would answer a different question.No customers match «nortwind»
Archived customers are not searched by default.An empty card that is good news
Inside a card,
size="sm"withvariant="ghost"keeps the emptiness from drawing a second frame and a big picture. Theiconslot puts a green shield instead of a blank page — no disputes is the answer the reader hoped for — andheading-levelfollows the card’s own heading.Recent payments
Northwind2 min ago€1,284.50Contoso1 h ago€432.00Fabrikamyesterday€96.00Open disputes
No open disputes
Chargebacks appear here within an hour of the bank's notice.
API generated from the package · cannot drift
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 | The scale of the icon, the text and the spacing. `md` by default; taken from `GrConfigProvider`. |
description | string | undefined | undefined | An explanation under the heading: why it is empty and what to do. The `#description` slot is stronger. |
headingLevel | 2 | 3 | 4 | 5 | 6 | undefined | undefined | The heading level (`h2`…`h6`) to fit the structure of the page. `3` by default. |
kind | "search" | "empty" | undefined | "empty" | What kind of emptiness it is: nothing has been created yet (`empty`) or nothing was found for the query (`search`). Changes the default icon and title; a given `title` is stronger in both cases. |
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 | The explanation as markup — with a link or an emphasis; stronger than `description`. |
default | any | The actions under the text: they are centred. |
Types
Every exported type of the subpath, grouped by the file it lives in. Names, generics and bodies come from the package’s declaration files and cannot drift.
import type { GrEmptyStateProps, GrEmptyStateConfigurableProps, GrEmptyStateSize } from '@feugene/granularity/components/GrEmptyState'Filedist/types/components/GrEmptyState/GrEmptyState.vue.d.ts
interface GrEmptyStateProps {
title?: string;
description?: string;
size?: GrEmptyStateSize;
variant?: GrEmptyStateVariant;
headingLevel?: GrEmptyStateHeadingLevel;
kind?: GrEmptyStateKind;
}Filedist/types/components/GrEmptyState/config.d.ts
const grEmptyStateConfig: import("@feugene/granum/contract").GranumComponentDescriptor<"GrEmptyState">;Filedist/types/components/GrEmptyState/defaults.d.ts
interface GrEmptyStateConfigurableProps {
size: GrEmptyStateSize;
variant: GrEmptyStateVariant;
headingLevel: GrEmptyStateHeadingLevel;
}Filedist/types/components/GrEmptyState/grEmptyStateStyles.d.ts
type GrEmptyStateSize = GrComponentSize;type GrEmptyStateVariant = typeof GR_EMPTY_STATE_VARIANTS[number];type GrEmptyStateKind = typeof GR_EMPTY_STATE_KINDS[number];type GrEmptyStateHeadingLevel = typeof GR_EMPTY_STATE_HEADING_LEVELS[number];Filedist/types/components/GrEmptyState/safelist.d.ts
const grEmptyStateSafelist: string[];Usage from the library prose
Two emptinesses, not one
kind tells apart what the user sees differently:
kind | When | Default heading and icon |
|---|---|---|
empty (the default) | nothing has been created yet | “Nothing here yet”, a box |
search | nothing was found for the query or the filter | “Nothing found”, a magnifier with a cross |
The difference is not cosmetic. Under a filter “Nothing here yet” misinforms: the data is there, it merely did not match, and the user’s next step is different — not “create” but “loosen the query”.
There are exactly two values because that is how many recur. A loading error is deliberately
not among them: the package has components of its own for it —
GrResponseErrorBanner.
A default description is not substituted in either kind: it is always about a specific screen, and there is nothing to guess it from.
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.
Props in this example