Empty State
Shown where content would be, explaining why it is empty and what to do next.
No projects yet
Projects group your deployments and environments. Create one to get started.
Installation
pnpm dlx @dowel-ui/cli add empty-statenpm packages installed: class-variance-authority.
Accessibility
A plain container with no implicit landmark or live region. When it replaces content after a search, put aria-live on the results region so the change is announced. The icon is decorative — the title has to carry the message on its own.
Props
EmptyState
| Prop | Type | Default |
|---|---|---|
bordered | boolean | — |
size | "sm" | "md" | "lg" | "md" |
Plus every attribute of <div>.
EmptyStateIcon
Plus every attribute of <div>.
EmptyStateTitle
Plus every attribute of <p>.
EmptyStateDescription
Plus every attribute of <p>.
EmptyStateActions
Plus every attribute of <div>.
Quality
7/7 checks, measured from the source and its tests
- Tested — passes
- axe assertion — passes
- Keyboard tested — does not apply
- Storybook examples — passes
- Accessibility documented — passes
- Semantic tokens only — passes
- Motion from tokens — does not apply
- className merged — passes
- Visible focus — does not apply
- No fixed widths — passes
Used in
Whole screens assembled from this component. Installing one brings this and everything else it needs with it.
- Admin — users
- Admin dashboard
- Agent console
- AI Chat
- AI dashboard
- AI workspace
- Billing
- Command center
- CRM
- Forgot password
Source
This is exactly what dowel add empty-state writes into your project, with imports rewritten to your own path alias.
import { cva, type VariantProps } from "class-variance-authority";
import type { ComponentPropsWithRef } from "react";
import { cn } from "@/lib/utils";
const emptyStateVariants = cva("flex flex-col items-center justify-center gap-3 text-center", {
variants: {
size: {
sm: "px-4 py-8",
md: "px-6 py-12",
lg: "px-8 py-20",
},
bordered: {
true: "rounded-xl border border-dashed border-border",
false: "",
},
},
defaultVariants: {
size: "md",
bordered: false,
},
});
export interface EmptyStateProps
extends ComponentPropsWithRef<"div">, VariantProps<typeof emptyStateVariants> {}
/**
* Shown where content would be, when there is none.
*
* An empty state should say why it is empty and what to do next. "No results"
* after a search and "nothing here yet" on a new account are different
* messages, and the second one is where a call to action belongs.
*/
export function EmptyState({ className, size, bordered, ...props }: EmptyStateProps) {
return (
<div
data-slot="empty-state"
className={cn(emptyStateVariants({ size, bordered }), className)}
{...props}
/>
);
}
/** Decorative by default — the title carries the meaning. */
export function EmptyStateIcon({ className, ...props }: ComponentPropsWithRef<"div">) {
return (
<div
data-slot="empty-state-icon"
aria-hidden="true"
className={cn(
"grid size-11 place-items-center rounded-full bg-muted text-muted-foreground",
"[&_svg:not([class*='size-'])]:size-5",
className,
)}
{...props}
/>
);
}
export function EmptyStateTitle({ className, children, ...props }: ComponentPropsWithRef<"p">) {
return (
<p
data-slot="empty-state-title"
className={cn("text-base font-medium text-foreground", className)}
{...props}
>
{children}
</p>
);
}
export function EmptyStateDescription({ className, ...props }: ComponentPropsWithRef<"p">) {
return (
<p
data-slot="empty-state-description"
className={cn("max-w-sm text-sm text-balance text-muted-foreground", className)}
{...props}
/>
);
}
export function EmptyStateActions({ className, ...props }: ComponentPropsWithRef<"div">) {
return (
<div
data-slot="empty-state-actions"
className={cn("mt-1 flex flex-wrap items-center justify-center gap-2", className)}
{...props}
/>
);
}
export { emptyStateVariants };