Empty State
Displays a placeholder when content is empty or unavailable.
Overview
The EmptyState component provides a consistent way to communicate that a section has no content to display. By default it fills the available parent space, centers its content, and renders a bordered surface shell.
Use variant="ghost" when the parent already owns the surrounding shell. Use variant="overlay" when the empty state should sit above existing content, such as a filtered table or a loading result set.
Usage
import {
EmptyState,
EmptyStateDescription,
EmptyStateHeading,
EmptyStateMedia,
EmptyStateTitle,
} from "@tilt-legal/cubitt-components/empty-state";
import { Inbox } from "@tilt-legal/cubitt-icons/ui/outline";<EmptyState>
<EmptyStateMedia>
<Inbox />
</EmptyStateMedia>
<EmptyStateHeading>
<EmptyStateTitle>No projects found</EmptyStateTitle>
<EmptyStateDescription>
Create your first project to get started with your workflow.
</EmptyStateDescription>
</EmptyStateHeading>
</EmptyState>Surface Behavior
default owns a bordered surface at parent +2. ghost does not advance or render a surface, so it is best inside another component that already owns structure and spacing. overlay is absolutely positioned and uses the current parent surface for its contextual gradient so it blends into the content underneath. If an overlay is mounted in a custom stack and needs a different elevation, override it with className.
ghost remains the correct name for EmptyState because it means “no shell.” Dropzone uses minimal for its own upload-target variant, where pointer and drag states still belong to Dropzone.
Examples
Variants
Switch between the default, ghost, and overlay variants. The overlay preview keeps the table header visible and covers only the skeleton body rows.
const variants = ["default", "ghost", "overlay"] as const;
const [variant, setVariant] =
useState<"default" | "ghost" | "overlay">("default");
<>
{variant === "overlay" ? (
<div className="relative overflow-hidden">
<Table>
<TableHeader>{/* header stays visible */}</TableHeader>
<TableBody>{/* skeleton rows stay mounted underneath */}</TableBody>
</Table>
<EmptyState className="top-11 h-auto p-8" variant="overlay">
<EmptyStateMedia>
<Magnifier />
</EmptyStateMedia>
<EmptyStateHeading>
<EmptyStateTitle>No matching matters</EmptyStateTitle>
<EmptyStateDescription>
Try adjusting your filters or search terms.
</EmptyStateDescription>
</EmptyStateHeading>
</EmptyState>
</div>
) : (
<EmptyState className="h-[268px]" variant={variant}>
<EmptyStateMedia>
<Inbox />
</EmptyStateMedia>
<EmptyStateHeading>
<EmptyStateTitle>No projects found</EmptyStateTitle>
<EmptyStateDescription>
Create your first project to get started with your workflow.
</EmptyStateDescription>
</EmptyStateHeading>
</EmptyState>
)}
<PreviewFooter>
<ToggleGroup
aria-label="Empty state variant"
groupVariant="segmented"
multiple={false}
onValueChange={(value) => {
if (variants.includes(value as (typeof variants)[number])) {
setVariant(value as (typeof variants)[number]);
}
}}
value={variant}
>
<ToggleGroupItem value="default">Default</ToggleGroupItem>
<ToggleGroupItem value="ghost">Ghost</ToggleGroupItem>
<ToggleGroupItem value="overlay">Overlay</ToggleGroupItem>
</ToggleGroup>
</PreviewFooter>
</>;With Action Button
Use EmptyStateAction to add a call-to-action inside the empty state.
import { Button } from "@tilt-legal/cubitt-components/button";
import {
EmptyState,
EmptyStateAction,
EmptyStateDescription,
EmptyStateHeading,
EmptyStateMedia,
EmptyStateTitle,
} from "@tilt-legal/cubitt-components/empty-state";
import { File, Plus } from "@tilt-legal/cubitt-icons/ui/outline";
<EmptyState>
<EmptyStateMedia>
<File />
</EmptyStateMedia>
<EmptyStateHeading>
<EmptyStateTitle>No documents</EmptyStateTitle>
<EmptyStateDescription>
Upload or create your first document to get started.
</EmptyStateDescription>
</EmptyStateHeading>
<EmptyStateAction>
<Button onClick={() => undefined}>
<Plus />
Create Document
</Button>
</EmptyStateAction>
</EmptyState>Error State
Set error on EmptyState to apply error styling to EmptyStateMedia and EmptyStateTitle.
import { Button } from "@tilt-legal/cubitt-components/button";
import {
EmptyState,
EmptyStateAction,
EmptyStateDescription,
EmptyStateHeading,
EmptyStateMedia,
EmptyStateTitle,
} from "@tilt-legal/cubitt-components/empty-state";
import {
RefreshAnticlockwise,
TriangleWarning,
} from "@tilt-legal/cubitt-icons/ui/outline";
<EmptyState error>
<EmptyStateMedia>
<TriangleWarning />
</EmptyStateMedia>
<EmptyStateHeading>
<EmptyStateTitle>Something went wrong</EmptyStateTitle>
<EmptyStateDescription>
We couldn't load your data. Please try again.
</EmptyStateDescription>
</EmptyStateHeading>
<EmptyStateAction>
<Button onClick={() => undefined} variant="secondary">
<RefreshAnticlockwise />
Try Again
</Button>
</EmptyStateAction>
</EmptyState>API Reference
EmptyState
The root container component. By default it fills the available parent space with centered content and generous padding.
| Prop | Type | Default | Description |
|---|---|---|---|
variant | "default" | "ghost" | "overlay" | "default" | default renders a bordered shell, ghost removes shell styling, and overlay positions the state absolutely with a contextual parent-level gradient |
error | boolean | false | Enables error styling for EmptyStateMedia and EmptyStateTitle |
className | string | - | Additional CSS classes. Use this to adjust fill, spacing, positioning, or overlay elevation |
children | React.ReactNode | - | Child components |
EmptyStateMedia
Container for the icon with a circular background. Automatically applies error styling when the parent has error.
| Prop | Type | Default | Description |
|---|---|---|---|
size | "sm" | "md" | "lg" | "md" | Controls the media circle and icon size |
className | string | - | Additional CSS classes |
children | React.ReactNode | - | Icon element |
EmptyStateHeading
Container for the title and description.
| Prop | Type | Default | Description |
|---|---|---|---|
className | string | - | Additional CSS classes |
children | React.ReactNode | - | Title and description |
EmptyStateTitle
The title text. Automatically applies error styling when the parent has error.
| Prop | Type | Default | Description |
|---|---|---|---|
className | string | - | Additional CSS classes |
children | React.ReactNode | - | Title content |
EmptyStateDescription
The description text.
| Prop | Type | Default | Description |
|---|---|---|---|
className | string | - | Additional CSS classes |
children | React.ReactNode | - | Description content |
EmptyStateAction
Container for action buttons.
| Prop | Type | Default | Description |
|---|---|---|---|
className | string | - | Additional CSS classes |
children | React.ReactNode | - | Button element(s) |
Design Guidelines
- Use
defaultfor standalone empty panels. - Use
ghostwhen a parent component already provides the surface, border, padding, or interaction frame. - Use
overlaywhen the underlying content should remain mounted underneath the empty state. - Include a clear title and a short description that explains what happened or what the user can do next.
- Add an action when there is an obvious next step.
- Use the error state for failed data loading, not validation errors.