Empty State React component for Tailwind CSS v4
v1.0.0Added in 1.0.0
A React empty state for lists, tables, searches and pages with nothing to show yet: an icon tile, a title, a description and actions.
It appears with the tile scaling up and the text and buttons rising in one after another; under reduced motion it only fades. Pick a preset to swap icon, tone and copy at once. Press the retry button on the error preset and it spins until the returned promise settles. Tab moves through the actions in reading order, Enter or Space presses them. With live on, a screen reader announces the title and description once when the state appears. On a phone, or anywhere narrower than 400px, two or more actions stack full width with the primary one on top, and lg pages drop to the md spacing below 640px.
Empty State is a free React component in the React feedback components category, written in TypeScript for React 19 and Tailwind CSS v4. It installs as source with npx wingo-ui@latest add empty-state, together with Button, Motion presets, Tones and UIProvider and the npm packages @radix-ui/react-slot, motion and lucide-react.
Add to a project
Requirements: React 19 and Tailwind CSS v4, in Next.js (App Router or Pages), Vite or React Router. Installation guide · Next.js setup · Vite setup
npx wingo-ui@latest add empty-stateUsage
Import EmptyState from @/components/ui/empty-state.
import { FileUp, UserPlus } from "lucide-react"
import { Button } from "@/components/ui/button"
import { EmptyState } from "@/components/ui/empty-state"
export function ClientsEmpty({ onReload }: { onReload: () => Promise<void> }) {
return (
<div className="flex flex-col gap-4">
<EmptyState
variant="surface"
title="No clients yet"
description="Add your first client or import the list from a CSV to get started."
actions={
<>
<Button variant="outline" leftIcon={<FileUp />}>Import CSV</Button>
<Button leftIcon={<UserPlus />}>Add client</Button>
</>
}
/>
<EmptyState
preset="error"
size="sm"
title="We could not load the invoices"
description="Check your connection and try again."
onRetry={onReload}
/>
</div>
)
}Props
presetdefault"empty""empty" | "no-results" | "error" | "first-run" | "no-permission" | "offline"Picks the default icon, tone and copy for the usual reasons: empty (Inbox, neutral), no-results (SearchX, neutral), error (CircleAlert, danger), first-run (Sparkles, brand, with a plus badge), no-permission (Lock, warning), offline (WifiOff, neutral). Every piece stays overridable.
titledefault""ReactNodeThe heading. Leave empty for the preset's; null hides it.
descriptiondefault""ReactNodeOne or two sentences under the title; wraps at 36ch (44ch at lg). Leave empty for the preset's; null hides it.
iconReactNodeThe glyph inside the tile, sized by the empty state. Defaults to the preset's icon; null hides the tile.
illustrationReactNodeReplaces the tile with an image or an svg, at most 112 / 160 / 192px tall by size. Decorative (aria-hidden).
badgeReactNodeA small round badge on the tile's corner in the tone's fill. first-run shows a plus; null hides it.
tonedefault"""neutral" | "brand" | "success" | "warning" | "danger" | "info"The tile tint. Empty uses the preset's tone. neutral is a quiet gray tile; the others use the tone's soft fill and text.
colordefault""stringAny CSS color for the tile (and the badge); replaces the tone.
sizedefault"md""sm" | "md" | "lg"sm inside a table, list or popover (40px tile, 14px title, 24px padding); md in a panel (48px tile, 15px title, 40px padding); lg for a whole page (64px tile, 18px title, 64px padding, 40px below 640px).
variantdefault"plain""plain" | "dashed" | "surface"plain sits on whatever is behind it; dashed draws a dashed frame (first-run areas, drop zones); surface is a raised panel.
aligndefault"center""center" | "start"start aligns text and actions to the start edge, for cards and sidebars.
actionsReactNodeButtons. Put the primary action last in the DOM: it sits at the end of the row and on top when the row stacks.
onRetry() => (void | Promise<unknown>)Adds an outline retry button after the actions (meant for the error and offline presets). A returned promise keeps it loading until it settles.
retryLabeldefault"Try again"stringThe retry button's label.
titleAsdefault"h3""h2" | "h3" | "h4" | "p"The title's element, to fit the page's heading outline.
livedefaultfalsebooleanrole="status" with a polite live region, so a screen reader announces the state when it appears. Use it for results that change, like no-results under a search.
animateddefaulttruebooleanTile, text and actions rise in with a short stagger when the state mounts. Server-rendered markup shows the final state; reduced motion fades only. The demo's replay button remounts it.
presetsPartial<Record<EmptyStatePreset, Partial<EmptyStatePresetConfig>>>Overrides merged over DEFAULT_EMPTY_STATE_PRESETS ({ icon, tone, title, description, badge } per preset). Translate once for the whole app with <UIProvider defaults={{ "empty-state": { presets } }}>.
childrenReactNodeExtra content between the description and the actions: suggestions, a search field, a list of shortcuts.
classNamesPartial<Record<"media" | "icon" | "badge" | "title" | "description" | "content" | "actions", string>>Extra classes for the inner parts: the tile or illustration, the glyph, the corner badge, the title, the description, the children wrapper, the actions row.
classNamestringExtra classes for the root, merged last.
Parts
EmptyStateIcon
componentThe decorative icon tile on its own (data-slot empty-state-media). Inside an EmptyState it follows its size and entrance. Props: { tone?, color?, badge?, size?, classNames?: { icon, badge } } & div props.
EmptyStateTitle
componentThe title part, sized by the surrounding EmptyState. Props: { as?: "h2" | "h3" | "h4" | "p" } & heading props.
EmptyStateDescription
componentThe description part, sized by the surrounding EmptyState. Takes p props.
EmptyStateActions
componentThe actions row (wraps, stacks full width below 400px). asChild renders your own element as the row. Props: { asChild?: boolean } & div props.
DEFAULT_EMPTY_STATE_PRESETS
constantThe English preset copy, icons and tones, to spread and translate. Type: Record<EmptyStatePreset, EmptyStatePresetConfig>.
Examples
All six presets
Each preset brings its icon, tone and copy; the demo passes its own copy through presets.
No results in a search
Type a city: the list and the empty state swap with a fade, and the clear action empties the query.
Error with retry
onRetry returns a promise, so the button spins. The first retry fails, the second loads.
First run with an illustration
dashed, lg, brand tone and a fanned photo stack in place of the tile.
Start aligned in a card
align="start" for sidebars and widgets, with a custom icon.
Inside a table
size="sm" in a full-width cell under a real header.
Sizes and a custom color
sm, md and lg side by side, tinted with color="#7C3AED".
Files
- components/ui/empty-state.tsx
FAQ
Is the React Empty State component free?
Yes. The source is free and needs no account: copy it from the Code tab or install it with the CLI.
How do I install Empty State?
Run npx wingo-ui@latest add empty-state. It also adds the Wingo UI items it builds on (Button, Motion presets, Tones and UIProvider) and the npm packages @radix-ui/react-slot, motion and lucide-react. With shadcn, run npx shadcn@latest add https://wingo-ui.com/r/empty-state.json.
How does Empty State behave on phones?
On a phone, or anywhere narrower than 400px, two or more actions stack full width with the primary one on top, and lg pages drop to the md spacing below 640px.
See also
Used in
More React feedback components
Articles using this component
- React Empty State Component: First Use, No Results, Errors
Component guides8 min read
- React Skeleton Loader: Real Shapes, No Layout Shift
Component guides9 min read
- React 404 Page: 5 Error Screens for the Next.js App Router
Build it: screens and apps11 min read