Product
Empty state
Empty-page and empty-table layouts with a message and next actions.
$ npx context-ds add empty-stateUsage
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.