Skip to main content

Empty

Display a placeholder when there is no data or content to show. Commonly used inside tables, lists, or pages that have no results.

When to use

An Empty state tells the reader a region has no data, no results, or no access to show.

  • Prefer Skeleton while the region is still loading.
  • Prefer Alert when a message sits beside content that still exists.
  • Compose Empty.Root inside a Well when the state needs an inset panel.

No endpoints yet

Create your first endpoint to get started.

import { Empty } from "@ngrok/mantle/empty";import { Button } from "@ngrok/mantle/button";import { GhostIcon, PlusIcon } from "@phosphor-icons/react"; <Empty.Root>	<Empty.Icon svg={<GhostIcon />} />	<Empty.Title>No endpoints yet</Empty.Title>	<Empty.Description>		<p>Create your first endpoint to get started.</p>	</Empty.Description>	<Empty.Actions>		<Button type="button" appearance="filled" intent="neutral" icon={<PlusIcon />}>			Create endpoint		</Button>	</Empty.Actions></Empty.Root>;

Error page

A full-page error state with a retry action.

Oops, something went wrong.

Please try again in a few minutes.

import { Empty } from "@ngrok/mantle/empty";import { Button } from "@ngrok/mantle/button";import { SmileyMeltingIcon } from "@phosphor-icons/react"; <Empty.Root>	<Empty.Icon svg={<SmileyMeltingIcon />} />	<Empty.Title>Oops, something went wrong.</Empty.Title>	<Empty.Description>		<p>Please try again in a few minutes.</p>	</Empty.Description>	<Empty.Actions>		<Button type="button" appearance="outlined" intent="neutral">			Retry		</Button>	</Empty.Actions></Empty.Root>;

No filter results

A common pattern for empty states when a filter or search yields no results.

No results matched your filter.

Check your spelling. It's possible what you're looking for no longer exists.

import { Empty } from "@ngrok/mantle/empty";import { Button } from "@ngrok/mantle/button";import { MagnifyingGlassIcon } from "@phosphor-icons/react"; <Empty.Root>	<Empty.Icon svg={<MagnifyingGlassIcon />} />	<Empty.Title>No results matched your filter.</Empty.Title>	<Empty.Description>		<p>Check your spelling. It's possible what you're looking for no longer exists.</p>	</Empty.Description>	<Empty.Actions>		<Button type="button" appearance="outlined" intent="neutral">			Clear filters		</Button>	</Empty.Actions></Empty.Root>;

Within a data table

Inside a DataTable, host Empty in DataTable.EmptyRow. The row already spans every column (auto colSpan) and Empty.Root centers itself, so you don't hand-roll a <td> or any centering. Branch on whether a filter is active to choose between the "no data yet" and "no results" copy. See the DataTable empty-states recipe for the full two-state pattern.

import { Button } from "@ngrok/mantle/button";import { DataTable, createColumnHelper, tableFeatures, useTable } from "@ngrok/mantle/data-table";import { Empty } from "@ngrok/mantle/empty";import { MagnifyingGlassIcon } from "@phosphor-icons/react/MagnifyingGlass"; type Row = { id: string; name: string }; // The filter in this example lives outside the table, so the table registers// no features.const features = tableFeatures({}); const columnHelper = createColumnHelper<typeof features, Row>(); const columns = columnHelper.columns([	columnHelper.accessor("name", {		id: "name",		header: () => <DataTable.Header>Name</DataTable.Header>,		cell: (props) => <DataTable.Cell key={props.cell.id}>{props.getValue()}</DataTable.Cell>,	}),]); function EmptyInTableExample({ data }: { data: Row[] }) {	const table = useTable({ features, data, columns }); 	const rows = table.getRowModel().rows; 	const clearFilters = () => {		/* clear your filter state */	}; 	return (		<DataTable.Root table={table}>			<DataTable.Head />			<DataTable.Body>				{rows.length > 0 ? (					rows.map((row) => <DataTable.Row key={row.id} row={row} />)				) : (					<DataTable.EmptyRow>						<Empty.Root>							<Empty.Icon svg={<MagnifyingGlassIcon />} />							<Empty.Title>No results matched your filter.</Empty.Title>							<Empty.Description>								<p>Try a different search, or clear the filter to see everything.</p>							</Empty.Description>							<Empty.Actions>								<Button type="button" appearance="outlined" intent="neutral" onClick={clearFilters}>									Clear filters								</Button>							</Empty.Actions>						</Empty.Root>					</DataTable.EmptyRow>				)}			</DataTable.Body>		</DataTable.Root>	);}

Minimal

Use only the sub-components you need.

Nothing here

import { Empty } from "@ngrok/mantle/empty";import { GhostIcon } from "@phosphor-icons/react"; <Empty.Root>	<Empty.Icon svg={<GhostIcon />} />	<Empty.Title>Nothing here</Empty.Title></Empty.Root>;

Composition

Compose the parts of an Empty state together to build your own:

Empty.Root├── Empty.Icon├── Empty.Title├── Empty.Description└── Empty.Actions

Polymorphism

Empty.Root, Empty.Title, Empty.Description, and Empty.Actions accept asChild. When true the part renders its single child instead of its default element, forwarding class names, data-* attributes, and ref.

Reach for it when the empty state is a semantic landmark (Empty.Root asChild around a <section>), or when Empty.Title needs a heading level that fits the page outline rather than the default. Empty.Actions asChild is useful when the action row should be a <form> or a navigation element.

Empty.Icon is the documented exception: it takes its element through the svg prop and clones it, so polymorphism already goes through that prop rather than asChild.

API Reference

Empty.Root

The root container for an empty state. Centers content horizontally with consistent vertical padding and max-width.

All props from div, plus:

PropTypeDefaultDescription
asChild?booleanfalseRender as a different element by passing a child.

Empty.Icon

Renders a large icon for the empty state. Pass a single SVG icon element via the svg prop. The icon is sized to size-10 and is decorative by default: it renders aria-hidden unless the SVG carries aria-label, aria-labelledby, role, or an explicit aria-hidden. The title carries the meaning.

All props from svg, plus:

PropTypeDefaultDescription
svgReactNode—A single SVG icon element.

Empty.Title

The heading text for the empty state. Renders as an h3 by default. Use asChild to render as a different heading level.

All props from h3, plus:

PropTypeDefaultDescription
asChild?booleanfalseRender as a different heading element (e.g. h2, h3) by passing a child.

Empty.Description

Supporting descriptive text rendered below the title. Renders as a div with vertical spacing so multiple paragraphs can be placed inside.

All props from div, plus:

PropTypeDefaultDescription
asChild?booleanfalseRender as a different element by passing a child.

Empty.Actions

A flex container for action buttons or links.

All props from div, plus:

PropTypeDefaultDescription
asChild?booleanfalseRender as a different element by passing a child.