Patterns
Empty states
What to say and which action to offer on first use, when nothing matches and when everything is done.
An empty area should tell users what is going on and what comes next. This pattern separates three empty states: first use, no results and done.
When to use
Section titled “When to use”- First use: when the user has not created anything yet. One primary action starts the work.
- No results: when a search or filter matches no record. It says what was searched and offers to clear the filters.
- Done: when all items are handled. No action is needed.
Live example
Section titled “Live example”The three kinds side by side; each is an EmptyState. It has an icon, a heading that names the state, a one-sentence explanation and at most one action.
import { Button, Card, createDataTableColumnHelper, DataTable, EmptyState, Grid, type IconName } from '@bigbang/ui';import type { ReactNode } from 'react';import { type Lang, LangProvider } from '../lang';
const copy = { tr: { firstUse: { heading: 'Henüz proje yok', text: 'Projeler işi gruplar ve ekibinizle paylaşır.', action: 'Proje oluştur' }, noResults: { heading: '“Rapor” için sonuç yok', text: 'Yazımı denetleyin ya da filtreleri gevşetin.', action: 'Filtreleri temizle' }, cleared: { heading: 'Tüm öğeler işlendi', text: 'Bekleyen öğe kalmadı.' }, tableCaption: 'Proje listesi', tableName: 'Ad', tableEmpty: 'Henüz proje yok', }, en: { firstUse: { heading: 'No projects yet', text: 'Projects group work and share it with your team.', action: 'Create project' }, noResults: { heading: 'No results for “Report”', text: 'Check the spelling or loosen the filters.', action: 'Clear filters' }, cleared: { heading: 'All items are handled', text: 'No pending items are left.' }, tableCaption: 'Project list', tableName: 'Name', tableEmpty: 'No projects yet', },};
export type EmptyKind = 'first-use' | 'no-results' | 'cleared' | 'table';
/** One empty state: a decorative icon, a heading that names the state, one sentence and at most one primary action. */function Empty({ icon, heading, text, action }: { icon: IconName; heading: string; text: string; action?: string }) { return ( <Card variant="outlined" padding="md"> <EmptyState size="sm" headingLevel={3} icon={icon} title={heading} description={text} actions={action ? <Button>{action}</Button> : undefined} /> </Card> );}
type Row = { id: string; name: string };const helper = createDataTableColumnHelper<Row>();
/** The three kinds of empty state, built with EmptyState (in a Card here) and Button. Pass `kind` to show one. */export function EmptyStatesDemo({ lang = 'tr', kind }: { lang?: Lang; kind?: EmptyKind }) { const t = copy[lang]; const columns = [helper.accessor('name', { header: t.tableName, meta: { rowHeader: true } })]; const parts: Record<EmptyKind, ReactNode> = { 'first-use': <Empty icon="plus" heading={t.firstUse.heading} text={t.firstUse.text} action={t.firstUse.action} />, 'no-results': <Empty icon="search" heading={t.noResults.heading} text={t.noResults.text} action={t.noResults.action} />, cleared: <Empty icon="circle-check" heading={t.cleared.heading} text={t.cleared.text} />, table: <DataTable<Row> columns={columns} data={[]} getRowId={(row) => row.id} caption={t.tableCaption} captionHidden status="empty" messages={{ empty: t.tableEmpty }} />, }; return ( <LangProvider lang={lang}> {kind ? ( <div className="max-w-md">{parts[kind]}</div> ) : ( <Grid columns={{ base: 1, md: 3 }} gap="lg"> {parts['first-use']} {parts['no-results']} {parts.cleared} </Grid> )} </LangProvider> );}An empty table never shows the header row alone. When DataTable has the status empty, it shows one message row.
No projects yet |
import { Button, Card, createDataTableColumnHelper, DataTable, EmptyState, Grid, type IconName } from '@bigbang/ui';import type { ReactNode } from 'react';import { type Lang, LangProvider } from '../lang';
const copy = { tr: { firstUse: { heading: 'Henüz proje yok', text: 'Projeler işi gruplar ve ekibinizle paylaşır.', action: 'Proje oluştur' }, noResults: { heading: '“Rapor” için sonuç yok', text: 'Yazımı denetleyin ya da filtreleri gevşetin.', action: 'Filtreleri temizle' }, cleared: { heading: 'Tüm öğeler işlendi', text: 'Bekleyen öğe kalmadı.' }, tableCaption: 'Proje listesi', tableName: 'Ad', tableEmpty: 'Henüz proje yok', }, en: { firstUse: { heading: 'No projects yet', text: 'Projects group work and share it with your team.', action: 'Create project' }, noResults: { heading: 'No results for “Report”', text: 'Check the spelling or loosen the filters.', action: 'Clear filters' }, cleared: { heading: 'All items are handled', text: 'No pending items are left.' }, tableCaption: 'Project list', tableName: 'Name', tableEmpty: 'No projects yet', },};
export type EmptyKind = 'first-use' | 'no-results' | 'cleared' | 'table';
/** One empty state: a decorative icon, a heading that names the state, one sentence and at most one primary action. */function Empty({ icon, heading, text, action }: { icon: IconName; heading: string; text: string; action?: string }) { return ( <Card variant="outlined" padding="md"> <EmptyState size="sm" headingLevel={3} icon={icon} title={heading} description={text} actions={action ? <Button>{action}</Button> : undefined} /> </Card> );}
type Row = { id: string; name: string };const helper = createDataTableColumnHelper<Row>();
/** The three kinds of empty state, built with EmptyState (in a Card here) and Button. Pass `kind` to show one. */export function EmptyStatesDemo({ lang = 'tr', kind }: { lang?: Lang; kind?: EmptyKind }) { const t = copy[lang]; const columns = [helper.accessor('name', { header: t.tableName, meta: { rowHeader: true } })]; const parts: Record<EmptyKind, ReactNode> = { 'first-use': <Empty icon="plus" heading={t.firstUse.heading} text={t.firstUse.text} action={t.firstUse.action} />, 'no-results': <Empty icon="search" heading={t.noResults.heading} text={t.noResults.text} action={t.noResults.action} />, cleared: <Empty icon="circle-check" heading={t.cleared.heading} text={t.cleared.text} />, table: <DataTable<Row> columns={columns} data={[]} getRowId={(row) => row.id} caption={t.tableCaption} captionHidden status="empty" messages={{ empty: t.tableEmpty }} />, }; return ( <LangProvider lang={lang}> {kind ? ( <div className="max-w-md">{parts[kind]}</div> ) : ( <Grid columns={{ base: 1, md: 3 }} gap="lg"> {parts['first-use']} {parts['no-results']} {parts.cleared} </Grid> )} </LangProvider> );}| Rule | Value |
|---|---|
| Primary action | 1 at most. “Create project” on first use; “Clear filters” for no results; none when done. |
| Heading | Names the state: “No projects yet”. Do not write “No data”. |
| Explanation | One sentence, 25 words at most. |
| No results | Shows the searched text in the heading: “No results for “Report””. |
| Empty table | The header grid is never shown alone; use status="empty" or "filtered-empty". |
| Illustration | Optional and decorative; it carries no meaning and is aria-hidden. |
| Heading level | Follows the page outline; headingLevel={3} in the example; the small size size="sm" fits inside a card. |
Behaviour
Section titled “Behaviour”- When data has loaded and there are no records, the kind of state is decided: nothing created, a filter matched nothing, or everything is handled.
- The matching empty state takes the place of the list area. The page title and filter bar stay where they are.
- The primary action starts the work or clears the filters. After clearing, the list refills and focus moves to the first control.
- After first use, when the first record is created, the empty state turns into the list.
Accessibility
Section titled “Accessibility”- The heading is a real heading element; screen reader users find the state with heading navigation.
- The icon and any illustration carry
aria-hidden; meaning is given in text. - The action is a real button with a visible name.
- When the state follows a search or filter, the change is announced politely as in the Filtering pattern.
- Colour never carries meaning alone (WCAG 1.4.1).
Content
Section titled “Content”- The heading names the state: “No projects yet”, “All items are handled”.
- The explanation gives the next step or the reason: “Projects group work and share it with your team.”
- For no results, quote the searched text and give a way out.
- Button text starts with a verb and has no full stop: “Create project”.
- Do not blame the user; do not write “You found nothing”.
Motion
Section titled “Motion”| Item | Motion | Preset |
|---|---|---|
| Empty state appearing | 110 ms fade; no shift | motion.reveal |
| Primary button | 0.97 scale on press | motion.press |
| After refresh and filter | No enter motion | none |
With reduced motion, scales and shifts are removed and only the short fade stays.
Components used
Section titled “Components used”EmptyState, Card, Button, DataTable. Related patterns: Filtering, Bulk actions.