Feedback
Empty state
What a list, table or page shows when there is nothing yet: a glyph, a title, one sentence and an action.
- Built on
- Client JavaScript
- None
- Works without React
- Yes, as HTML and CSS
- Source, gzipped
- 1.9 kB
- Dependencies
- React
No deploys yet
Each push to main lands here.
Try its options
Loading its options.
Code
import { Button } from "@/components/equip/button";
import { EmptyInbox, EmptyState, EmptyStateActions } from "@/components/equip/empty-state";
export default function Demo() {
return (
<EmptyState size="s" icon={<EmptyInbox />} title="No deploys yet">
Each push to main lands here.
<EmptyStateActions>
<Button size="s" variant="accent">Connect a repository</Button>
</EmptyStateActions>
</EmptyState>
);
}Your versionchange its options in Look, This part
HTMLworks in any framework, with the part's CSS
<div class="eq-empty-state" data-size="s" data-align="center">
<div class="eq-empty-state-icon">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" class="eq-empty-state-glyph">
<path d="M3.75 13.5l2.4-7.2a1.5 1.5 0 0 1 1.42-1.05h8.86a1.5 1.5 0 0 1 1.42 1.05l2.4 7.2" />
<path d="M3.75 13.5v4.25a1.5 1.5 0 0 0 1.5 1.5h13.5a1.5 1.5 0 0 0 1.5-1.5V13.5h-4.5l-1.25 2h-5l-1.25-2z" />
</svg>
</div>
<h3 class="eq-empty-state-title">No deploys yet</h3>
<div class="eq-empty-state-body">Each push to main lands here.<div class="eq-empty-state-actions"><button type="button" class="eq-button" data-variant="accent" data-size="s"><span class="eq-button-label">Connect a repository</span></button></div></div>
</div>Examples
The three glyphs
Say what will appear here and how to make the first one; for no results, say how to widen the search.
No invoices yet
Invoices appear here after your first billing cycle closes.
No matches for “eu-west-3”
Check the spelling or search all regions.
Logs did not load
The log service timed out after 30 seconds.
Code
import { Button } from "@/components/equip/button";
import { EmptyError, EmptyInbox, EmptySearch, EmptyState, EmptyStateActions } from "@/components/equip/empty-state";
export default function Example() {
return (
<div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fit, minmax(200px, 1fr))", gap: 16 }}>
<EmptyState size="s" icon={<EmptyInbox />} title="No invoices yet">
Invoices appear here after your first billing cycle closes.
</EmptyState>
<EmptyState size="s" icon={<EmptySearch />} title="No matches for “eu-west-3”">
Check the spelling or search all regions.
<EmptyStateActions>
<Button size="s" variant="outline">Clear filters</Button>
</EmptyStateActions>
</EmptyState>
<EmptyState size="s" icon={<EmptyError />} title="Logs did not load">
The log service timed out after 30 seconds.
<EmptyStateActions>
<Button size="s" variant="outline">Try again</Button>
</EmptyStateActions>
</EmptyState>
</div>
);
}HTMLworks in any framework, with the part's CSS
<div style="display:grid;grid-template-columns:repeat(auto-fit, minmax(200px, 1fr));gap:16px">
<div class="eq-empty-state" data-size="s" data-align="center">
<div class="eq-empty-state-icon">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" class="eq-empty-state-glyph">
<path d="M3.75 13.5l2.4-7.2a1.5 1.5 0 0 1 1.42-1.05h8.86a1.5 1.5 0 0 1 1.42 1.05l2.4 7.2" />
<path d="M3.75 13.5v4.25a1.5 1.5 0 0 0 1.5 1.5h13.5a1.5 1.5 0 0 0 1.5-1.5V13.5h-4.5l-1.25 2h-5l-1.25-2z" />
</svg>
</div>
<h3 class="eq-empty-state-title">No invoices yet</h3>
<div class="eq-empty-state-body">Invoices appear here after your first billing cycle closes.</div>
</div>
<div class="eq-empty-state" data-size="s" data-align="center">
<div class="eq-empty-state-icon">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" class="eq-empty-state-glyph">
<circle cx="10.5" cy="10.5" r="6" />
<path d="M15 15l5.25 5.25M8.5 10.5h4" />
</svg>
</div>
<h3 class="eq-empty-state-title">No matches for “eu-west-3”</h3>
<div class="eq-empty-state-body">Check the spelling or search all regions.<div class="eq-empty-state-actions"><button type="button" class="eq-button" data-variant="outline" data-size="s"><span class="eq-button-label">Clear filters</span></button></div></div>
</div>
<div class="eq-empty-state" data-size="s" data-align="center">
<div class="eq-empty-state-icon">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" class="eq-empty-state-glyph">
<path d="M6.25 3.75h8l3.5 3.5v6.5l-2 1.5-2-1.5-2 1.5-2-1.5-2 1.5-1.5-1.1V5.25a1.5 1.5 0 0 1 1.5-1.5z" />
<path d="M17.75 17.5v1.25a1.5 1.5 0 0 1-1.5 1.5h-8.5a1.5 1.5 0 0 1-1.5-1.5V17.5" />
<path d="M14.25 3.75v3.5h3.5" />
</svg>
</div>
<h3 class="eq-empty-state-title">Logs did not load</h3>
<div class="eq-empty-state-body">The log service timed out after 30 seconds.<div class="eq-empty-state-actions"><button type="button" class="eq-button" data-variant="outline" data-size="s"><span class="eq-button-label">Try again</span></button></div></div>
</div>
</div>Page level with two actions
headingLevel keeps the outline right: use 2 when the empty state is the main content of the page.
No API keys
Keys let your services call the API. Create one per environment so you can rotate them separately.
Code
import { Button } from "@/components/equip/button";
import { EmptyInbox, EmptyState, EmptyStateActions } from "@/components/equip/empty-state";
export default function Example() {
return (
<EmptyState icon={<EmptyInbox />} headingLevel={2} title="No API keys">
Keys let your services call the API. Create one per environment so you can rotate them separately.
<EmptyStateActions>
<Button variant="accent">Create API key</Button>
<Button variant="ghost">Read the docs</Button>
</EmptyStateActions>
</EmptyState>
);
}HTMLworks in any framework, with the part's CSS
<div class="eq-empty-state" data-size="m" data-align="center">
<div class="eq-empty-state-icon">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" class="eq-empty-state-glyph">
<path d="M3.75 13.5l2.4-7.2a1.5 1.5 0 0 1 1.42-1.05h8.86a1.5 1.5 0 0 1 1.42 1.05l2.4 7.2" />
<path d="M3.75 13.5v4.25a1.5 1.5 0 0 0 1.5 1.5h13.5a1.5 1.5 0 0 0 1.5-1.5V13.5h-4.5l-1.25 2h-5l-1.25-2z" />
</svg>
</div>
<h2 class="eq-empty-state-title">No API keys</h2>
<div class="eq-empty-state-body">Keys let your services call the API. Create one per environment so you can rotate them separately.<div class="eq-empty-state-actions"><button type="button" class="eq-button" data-variant="accent" data-size="m"><span class="eq-button-label">Create API key</span></button><button type="button" class="eq-button" data-variant="ghost" data-size="m"><span class="eq-button-label">Read the docs</span></button></div></div>
</div>Start aligned in a panel
No webhooks
Add an endpoint to hear about deploys as they finish.
Code
import { Button } from "@/components/equip/button";
import { EmptyState, EmptyStateActions } from "@/components/equip/empty-state";
export default function Example() {
return (
<div style={{ maxWidth: 320, padding: 16, border: "1px solid var(--eq-line)", borderRadius: 8 }}>
<EmptyState size="s" align="start" headingLevel={4} title="No webhooks">
Add an endpoint to hear about deploys as they finish.
<EmptyStateActions>
<Button size="s" variant="outline">Add endpoint</Button>
</EmptyStateActions>
</EmptyState>
</div>
);
}HTMLworks in any framework, with the part's CSS
<div style="max-width:320px;padding:16px;border:1px solid var(--eq-line);border-radius:8px">
<div class="eq-empty-state" data-size="s" data-align="start">
<h4 class="eq-empty-state-title">No webhooks</h4>
<div class="eq-empty-state-body">Add an endpoint to hear about deploys as they finish.<div class="eq-empty-state-actions"><button type="button" class="eq-button" data-variant="outline" data-size="s"><span class="eq-button-label">Add endpoint</span></button></div></div>
</div>
</div>Whole screen
size="l" is for a screen with nothing else on it: a 96px tile filled with the page colour and a title that grows with the space around it.
No messages in this project
Messages from your team and from clients arrive here. Invite someone to start the first thread.
Code
import { Button } from "@/components/equip/button";
import { EmptyInbox, EmptyState, EmptyStateActions } from "@/components/equip/empty-state";
export default function Example() {
return (
<EmptyState size="l" headingLevel={2} title="No messages in this project" icon={<EmptyInbox />} style={{ width: "100%" }}>
Messages from your team and from clients arrive here. Invite someone to start the first thread.
<EmptyStateActions>
<Button variant="accent">Invite people</Button>
<Button variant="ghost">Import from email</Button>
</EmptyStateActions>
</EmptyState>
);
}HTMLworks in any framework, with the part's CSS
<div style="width:100%" class="eq-empty-state" data-size="l" data-align="center">
<div class="eq-empty-state-icon">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" class="eq-empty-state-glyph">
<path d="M3.75 13.5l2.4-7.2a1.5 1.5 0 0 1 1.42-1.05h8.86a1.5 1.5 0 0 1 1.42 1.05l2.4 7.2" />
<path d="M3.75 13.5v4.25a1.5 1.5 0 0 0 1.5 1.5h13.5a1.5 1.5 0 0 0 1.5-1.5V13.5h-4.5l-1.25 2h-5l-1.25-2z" />
</svg>
</div>
<h2 class="eq-empty-state-title">No messages in this project</h2>
<div class="eq-empty-state-body">Messages from your team and from clients arrive here. Invite someone to start the first thread.<div class="eq-empty-state-actions"><button type="button" class="eq-button" data-variant="accent" data-size="m"><span class="eq-button-label">Invite people</span></button><button type="button" class="eq-button" data-variant="ghost" data-size="m"><span class="eq-button-label">Import from email</span></button></div></div>
</div>Props
| Prop | Type | Default | What it does |
|---|---|---|---|
title | ReactNode | none | Says what is missing, such as "No deploys yet". |
children | ReactNode | none | One sentence, then an optional EmptyStateActions. |
icon | ReactNode | none | A small glyph for the dashed tile, such as <EmptyInbox />. |
headingLevel | 2 | 3 | 4 | 3 | The title's heading level, to fit the page outline. |
size | "s" | "m" | "l" | "m" | s is for a card, a panel or inside a table; m is for a page; l is for a whole screen, with a 96px tile filled with the page colour and a title from 20px to 26px. |
align | "center" | "start" | "center" | Centred, or aligned to the start edge for panels. "centre" is accepted too. |
...rest | ComponentProps<"div"> | none | Every native div attribute except title, which the part owns. |
Anatomy
| root | .eq-empty-state | Size and alignment are on data-size and data-align. |
|---|---|---|
| icon | .eq-empty-state-icon | A dashed tile holding the glyph. |
| glyph | .eq-empty-state-glyph | EmptyInbox, EmptySearch and EmptyError. |
| title | .eq-empty-state-title | An h2, h3 or h4. |
| body | .eq-empty-state-body | The sentence and the actions. |
| actions | .eq-empty-state-actions | EmptyStateActions: one or two buttons or links. |
Notes
- Renders on the server with no client JavaScript.
- Say what will appear here and how to make the first one: "Deploys appear after your first push" beats "Nothing here".
- For no results, name the query and offer a way to widen it rather than a way to create something.
- Give at most one primary action; a second action is quieter, such as a link to the docs.
- Glyphs are hidden from assistive tech, so the title must stand alone.
- Set headingLevel to fit the page outline; an empty state never needs an h1.
- A size l title scales with the nearest size container, or the viewport when there is none, so it stays in proportion on a phone and a wide screen.
Source
Two files, and they are the whole part. Open either to read it, copy it straight from its bar, or add the part to your loadout and install it with everything else you picked.
components/equip/empty-state.tsx94 lines
import type { ComponentProps, ReactNode } from "react";
import "./empty-state.css";
export type EmptyStateProps = Omit<ComponentProps<"div">, "title"> & {
/** Says what is missing, such as "No deploys yet". */
title: ReactNode;
/** A small drawn glyph, such as EmptyInbox. */
icon?: ReactNode;
headingLevel?: 2 | 3 | 4;
/** s for a card or panel, m for a page, l for a whole screen with nothing on it yet. */
size?: "s" | "m" | "l";
align?: "center" | "start" | "centre";
};
export function EmptyState({
title,
icon,
headingLevel = 3,
size = "m",
align = "center",
className,
children,
...rest
}: EmptyStateProps) {
const Heading = `h${headingLevel}` as const;
return (
<div
{...rest}
className={["eq-empty-state", className].filter(Boolean).join(" ")}
data-size={size}
data-align={align === "centre" ? "center" : align}
>
{icon != null && <div className="eq-empty-state-icon">{icon}</div>}
<Heading className="eq-empty-state-title">{title}</Heading>
{children != null && <div className="eq-empty-state-body">{children}</div>}
</div>
);
}
/** One or two actions, placed last inside EmptyState. */
export function EmptyStateActions({ className, ...rest }: ComponentProps<"div">) {
return <div {...rest} className={["eq-empty-state-actions", className].filter(Boolean).join(" ")} />;
}
type GlyphProps = ComponentProps<"svg">;
function Glyph({ className, children, ...rest }: GlyphProps) {
return (
<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden="true"
{...rest}
className={["eq-empty-state-glyph", className].filter(Boolean).join(" ")}
>
{children}
</svg>
);
}
/** An open tray: nothing has arrived. */
export function EmptyInbox(props: GlyphProps) {
return (
<Glyph {...props}>
<path d="M3.75 13.5l2.4-7.2a1.5 1.5 0 0 1 1.42-1.05h8.86a1.5 1.5 0 0 1 1.42 1.05l2.4 7.2" />
<path d="M3.75 13.5v4.25a1.5 1.5 0 0 0 1.5 1.5h13.5a1.5 1.5 0 0 0 1.5-1.5V13.5h-4.5l-1.25 2h-5l-1.25-2z" />
</Glyph>
);
}
/** A magnifier over nothing: a search or filter matched no results. */
export function EmptySearch(props: GlyphProps) {
return (
<Glyph {...props}>
<circle cx="10.5" cy="10.5" r="6" />
<path d="M15 15l5.25 5.25M8.5 10.5h4" />
</Glyph>
);
}
/** A torn page: something failed to load. */
export function EmptyError(props: GlyphProps) {
return (
<Glyph {...props}>
<path d="M6.25 3.75h8l3.5 3.5v6.5l-2 1.5-2-1.5-2 1.5-2-1.5-2 1.5-1.5-1.1V5.25a1.5 1.5 0 0 1 1.5-1.5z" />
<path d="M17.75 17.5v1.25a1.5 1.5 0 0 1-1.5 1.5h-8.5a1.5 1.5 0 0 1-1.5-1.5V17.5" />
<path d="M14.25 3.75v3.5h3.5" />
</Glyph>
);
}components/equip/empty-state.css112 lines
@layer equip {
.eq-empty-state {
--_tile: 48px;
--_glyph: 24px;
display: flex;
flex-direction: column;
align-items: center;
padding: var(--eq-space-6) var(--eq-space-4);
color: var(--eq-ink);
font: var(--eq-weight) var(--eq-text-s) / 1.45 var(--eq-font);
text-align: center;
}
.eq-empty-state[data-size="s"] {
--_tile: 32px;
--_glyph: 18px;
padding: 0;
}
/* Large: a whole screen. The title follows the nearest container's width, or the viewport. */
.eq-empty-state[data-size="l"] {
--_tile: 96px;
--_glyph: 64px;
padding-block: calc(var(--eq-space-6) * 1.5);
}
.eq-empty-state[data-align="start"] {
align-items: flex-start;
text-align: start;
}
/* A dashed tile: the outline of something that is not there yet. */
.eq-empty-state-icon {
display: grid;
place-items: center;
width: var(--_tile);
height: var(--_tile);
margin-bottom: var(--eq-space-3);
border: 1px dashed var(--eq-line-strong);
border-radius: var(--eq-radius-m);
color: var(--eq-ink-2);
}
.eq-empty-state[data-size="s"] .eq-empty-state-icon {
margin-bottom: var(--eq-space-2);
border-radius: var(--eq-radius-s);
}
.eq-empty-state-icon > svg,
.eq-empty-state-glyph {
width: var(--_glyph);
height: var(--_glyph);
}
/* The large tile is filled with the page colour so it reads on a tinted or patterned stage. */
.eq-empty-state[data-size="l"] .eq-empty-state-icon {
margin-bottom: var(--eq-space-4);
border-radius: var(--eq-radius-l);
background: var(--eq-bg);
}
/* The built-in glyphs are drawn on a 24 grid; a thinner stroke keeps them at about 2px when large. */
.eq-empty-state[data-size="l"] .eq-empty-state-glyph {
stroke-width: 0.75;
}
.eq-empty-state-title {
margin: 0;
font-size: var(--eq-text-l);
font-weight: var(--eq-weight-strong);
line-height: 1.3;
letter-spacing: -0.005em;
text-wrap: balance;
}
.eq-empty-state[data-size="s"] .eq-empty-state-title {
font-size: var(--eq-text-m);
}
.eq-empty-state[data-size="l"] .eq-empty-state-title {
font-size: clamp(calc(var(--eq-text-l) * 1.25), 4cqi, calc(var(--eq-text-l) * 1.625));
letter-spacing: -0.015em;
}
.eq-empty-state-body {
max-width: 44ch;
margin-top: var(--eq-space-1);
color: var(--eq-ink-2);
text-wrap: pretty;
}
.eq-empty-state-body > p {
margin: 0;
}
.eq-empty-state-actions {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: var(--eq-space-2);
margin-top: var(--eq-space-4);
}
.eq-empty-state[data-size="s"] .eq-empty-state-actions {
margin-top: var(--eq-space-3);
}
.eq-empty-state[data-align="start"] .eq-empty-state-actions {
justify-content: flex-start;
}
}