Empty state
Use after a list, a dashboard tile, or a search result page when there’s nothing yet. Empty states are an under-loved UX surface; small details here pay back in retention.
'use client';
import type { ReactNode } from 'react';import { Button, Card, Stack, Text } from '@helixui/core';import { Inbox } from '@helixui/icons';
export interface EmptyBlockProps { /** Icon — defaults to an Inbox glyph. Pass null for none. */ icon?: ReactNode; title: string; description?: ReactNode; actionLabel?: string; onAction?: () => void; /** Subtle secondary action, often "Learn more". */ secondaryLabel?: string; onSecondary?: () => void;}
export function EmptyBlock({ icon = <Inbox />, title, description, actionLabel, onAction, secondaryLabel, onSecondary,}: EmptyBlockProps) { return ( <Card variant="outlined" style={{ padding: 'var(--helixui-space-8)' }}> <Stack gap={4} align="center" style={{ textAlign: 'center' }}> {icon ? ( <div style={{ width: 48, height: 48, display: 'grid', placeItems: 'center', borderRadius: 'var(--helixui-radius-lg)', background: 'var(--helixui-color-bg-surface-subtle)', color: 'var(--helixui-color-text-muted)', }} aria-hidden > {icon} </div> ) : null} <Stack gap={1} align="center"> <Text size="lg" weight="semibold">{title}</Text> {description ? ( <Text size="sm" tone="muted" style={{ maxWidth: 360 }}>{description}</Text> ) : null} </Stack> {(actionLabel || secondaryLabel) ? ( <Stack direction="row" gap={2}> {secondaryLabel ? ( <Button variant="ghost" tone="neutral" onClick={onSecondary}>{secondaryLabel}</Button> ) : null} {actionLabel ? <Button onClick={onAction}>{actionLabel}</Button> : null} </Stack> ) : null} </Stack> </Card> );}Variants
// No content yet:<EmptyBlock title="No invoices yet" description="Once you generate your first invoice, it'll show up here." actionLabel="Create invoice" onAction={openNewInvoice}/>
// Search came back empty:<EmptyBlock title="No matches" description="Try a broader query or clear your filters." secondaryLabel="Clear filters" onSecondary={clearFilters}/>
// Permission denied:<EmptyBlock title="Access denied" description="You don't have permission to view this. Ask an admin to grant access." actionLabel="Request access" onAction={requestAccess}/>Design notes
- One primary action max. If the user has multiple things to try, the
copy in
descriptioncarries them; only the most likely goes in the button. - The icon is in a contained badge. Naked icons in empty states age poorly because they fight with the typography for attention.