GrEmptyState

Package: @feugene/granularitycoreGroup: feedback

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 #empty slot.

When to take something else

NeedTake
The data is loadingGrSkeleton / GrLoading
The request failedGrResponseErrorBanner
A warning in the flow of the pageGrAlert
Just text with no illustration is neededGrCard

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 (26, 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

SlotWhat for
titlethe heading as markup: a link, an emphasis, a counter
descriptionthe description as markup
iconan icon of your own instead of the built-in lucide/inbox
the default onethe 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 (xslg, 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…

Code
<GrEmptyState />

Install

npm i @feugene/granularity

Import

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

API

Props

PropTypedefaultDescription
variant"ghost" | "outlined" | undefinedundefined`ghost` removes the border and the background: a card inside a card draws a second border.
titlestring | undefinedundefinedThe heading. Unset — it comes from the locale; the `#title` slot is stronger than both.
size"xs" | "sm" | "md" | "lg" | undefinedundefined
descriptionstring | undefinedundefined
headingLevel2 | 3 | 4 | 5 | 6 | undefinedundefined

Slots

SlotTypeDescription
iconanyAn icon instead of the built-in one.
titleanyThe heading as markup: a link, an emphasis, a counter.
descriptionany
defaultanyThe actions under the text: they are centred.

Examples 3

Primary CTA inside card surface

No payouts yet

Create the first scheduled payout to start tracking approval and transfer states.

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

No saved views match “treasury”. Try a broader term or create a new filtered workspace.

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

Left rail can keep filters, contextual hints or a compact KPI while the main area uses `GrEmptyState`.

No team members invited

Inside an existing card the ghost variant drops the second border around the same surface.

Split Layout
<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>

Component documentationAll components