Empty stateDesign system
Product

Empty state

Empty-page and empty-table layouts with a message and next actions.

npx context-ds add empty-state
In the product
First visit
In table

Usage

Use it for

Pages and tables with nothing in them yet, or nothing matching a search.

Avoid

  • A blank panel
  • A lucide icon in place of the glyph tile

Example

examples/empty-state.tsx
"use client";import { useState } from "react";import { ExampleNameDialog } from "./demo-feedback";import { Plus } from "lucide-react";import { EmptyState, EmptyStateRow } from "@/components/product/empty-state";import { Button } from "@/components/ui/button";import { Table, TableBody, TableHead, TableHeader, TableRow } from "@/components/ui/table";import { GlyphTile } from "@/components/visuals/glyph/glyph-tile";export function Product() {  const [open, setOpen] = useState(false);  const [created, setCreated] = useState("");  if (created) return <div className="grid gap-3"><p role="status">{created} created in this example.</p><Button variant="outline" onClick={() => setCreated("")}>Reset example</Button></div>;  return (<>    <EmptyState      glyph={<GlyphTile name="apiKeys" className="size-16 p-2" />}      title="No API keys yet"      description="Use these keys to authenticate requests to the API. Keep them secret."      actions={        <Button variant="brand" size="sm" onClick={() => setOpen(true)}>          <Plus className="size-3.5" strokeWidth={1.75} />          Create API key        </Button>      }    />    <ExampleNameDialog title="Create API key" open={open} onOpenChange={setOpen} onSave={setCreated} submitLabel="Create" /></>  );}export function FirstVisit() {  const [open, setOpen] = useState(false);  const [created, setCreated] = useState("");  if (created) return <div className="grid gap-3"><p role="status">{created} created in this example.</p><Button variant="outline" onClick={() => setCreated("")}>Reset example</Button></div>;  return (<>    <EmptyState      glyph={<GlyphTile name="monitors" ringed className="size-28" />}      title="Watch a page or sitemap"      description="Create your first monitor to watch a page or sitemap for exact changes, or a whole site for changes that matter."      actions={        <>          <Button variant="brand" size="sm" onClick={() => setOpen(true)}>            <Plus className="size-3.5" strokeWidth={1.75} />            Create monitor          </Button>          <Button variant="outline" size="sm" asChild><a href="https://docs.context.dev/monitors" target="_blank" rel="noreferrer">Read docs</a></Button>        </>      }      footer={<span className="text-caption text-fg-subtle">0 of 5 monitors used</span>}    />    <ExampleNameDialog title="Create Monitor" open={open} onOpenChange={setOpen} onSave={setCreated} submitLabel="Create" /></>  );}export function InTable() {  return (    <div className="w-full max-w-2xl">      <Table>        <TableHeader>          <TableRow>            <TableHead>Monitor</TableHead>            <TableHead>Status</TableHead>            <TableHead>Last run</TableHead>          </TableRow>        </TableHeader>        <TableBody>          <EmptyStateRow colSpan={3}>No monitors match your search or filters.</EmptyStateRow>        </TableBody>      </Table>    </div>  );}

Source

components/product/empty-state.tsx
import * as React from "react";import { cn } from "@/lib/utils";// Never a blank page: a glyph tile (64px, or the 112px ringed story for a product's first visit), the title at h4, one// balanced line of muted text at most 480px wide, then the next actions (the brand button first) and anything quieter// under them, such as a usage line. Centered in the page column, which it fills when the page has nothing else.function EmptyState({  glyph,  title,  description,  actions,  footer,  fill = false,  as: Heading = "h2",  className,}: {  glyph?: React.ReactNode;  title: React.ReactNode;  description?: React.ReactNode;  actions?: React.ReactNode;  footer?: React.ReactNode;  /** Takes the page's height and centers itself in it, as on a page with nothing else to show. */  fill?: boolean;  /** h1 when the empty state is the whole page (it stands in for the page header). */  as?: "h1" | "h2" | "h3";  className?: string;}) {  return (    <div data-slot="empty-state" className={cn("flex flex-col items-center px-8 text-center", fill ? "my-auto flex-1 justify-center py-16" : "pt-8 pb-10", className)}>      {glyph}      <Heading className={cn("text-h4 leading-snug font-[450] tracking-normal text-fg", glyph && "mt-6")}>{title}</Heading>      {description && <p className="mt-1 max-w-120 text-body-sm text-balance text-fg-muted">{description}</p>}      {actions && <div className="mt-5 flex flex-wrap items-center justify-center gap-2">{actions}</div>}      {footer && <div className="mt-6">{footer}</div>}    </div>  );}/** Inside a table: the one row saying nothing matches, or nothing has happened yet, in quiet text. */function EmptyStateRow({ colSpan, className, children }: { colSpan: number; className?: string; children: React.ReactNode }) {  return (    <tr data-slot="empty-state-row">      <td colSpan={colSpan} className={cn("px-4 py-12 text-center text-body-sm text-fg-subtle", className)}>        {children}      </td>    </tr>  );}export { EmptyState, EmptyStateRow };

Reference: context-dashboard c0ddf7a src/components/monitors/monitors-home.tsx.