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 &ldquo;eu-west-3&rdquo;">
        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

PropTypeDefaultWhat it does
titleReactNodenoneSays what is missing, such as "No deploys yet".
childrenReactNodenoneOne sentence, then an optional EmptyStateActions.
iconReactNodenoneA small glyph for the dashed tile, such as <EmptyInbox />.
headingLevel2 | 3 | 43The 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.
...restComponentProps<"div">noneEvery native div attribute except title, which the part owns.

Anatomy

root.eq-empty-stateSize and alignment are on data-size and data-align.
icon.eq-empty-state-iconA dashed tile holding the glyph.
glyph.eq-empty-state-glyphEmptyInbox, EmptySearch and EmptyError.
title.eq-empty-state-titleAn h2, h3 or h4.
body.eq-empty-state-bodyThe sentence and the actions.
actions.eq-empty-state-actionsEmptyStateActions: 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;
  }
}

EqUIp is in beta: free for non-commercial use, no warranty, and things can change. Terms

Nothing picked yet. Press A on a part, or use the plus on the board, and the install command builds here.

Loading the index.

Loading the index