Skip to content
design
Components

Empty state

What a list, a table, a panel or a page shows when it has nothing to show, and the next step.

On this page

No checklist runs yet

Start one from a template, such as Leaving harbour or Night passage.
A checklist page with no runs: what is empty, why, and the one step that fills it.
Code
import { Button } from "@bosunkit/components/button";
import { EmptyState } from "@bosunkit/components/empty-state";
import { ClipboardList } from "lucide-react";
import type { ReactElement } from "react";

export const description: string =
  "A checklist page with no runs yet: the mark, what is empty, what to do, and the button that does it.";

export default function Showcase(): ReactElement {
  return (
    <EmptyState
      icon={ClipboardList}
      size="lg"
      headingLevel={2}
      title="No checklist runs yet"
      description="Start one from a template, such as Leaving harbour or Night passage."
      actions={
        <>
          <Button variant="primary">Start a run</Button>
          <Button>See the templates</Button>
        </>
      }
    />
  );
}

An empty state is a mark, one line saying what is empty, one saying why or what happens next, and the next step as a button or a link. Its kind says why there is nothing: empty (nothing recorded), no-results (a search or a filter matched nothing), unavailable (the data cannot be shown here, such as a tide where no tables cover the water) and done (nothing is left to do). Each kind draws its own mark; icon takes another, or null for none.

It is not a message: an empty log is not an error, so it wears no message color, and a done state takes the success green on its mark only. A lost connection to a system bosund reads is an alert, never an empty state. A no-results state reads its title out politely when it appears in the place of results while the crew type; announce turns that on or off for any kind. headingLevel makes the title a heading for a state that stands for a section or a page, and fill centers it in the height of its container. On a phone its actions stack to the screen's width.

Examples

A search that matched nothing

No crew member matches “Ana”

Check the spelling, or search by nickname or role.
A crew search that matched nobody: the state is read out as it replaces the results, and offers to clear the search.
Code
import { Button } from "@bosunkit/components/button";
import { EmptyState } from "@bosunkit/components/empty-state";
import type { ReactElement } from "react";

export const description: string =
  "A crew search that matched nobody: the state is read out as it replaces the results, and offers to clear the search.";

export default function NoResults(): ReactElement {
  return (
    <EmptyState
      kind="no-results"
      title="No crew member matches “Ana”"
      description="Check the spelling, or search by nickname or role."
      actions={<Button>Clear the search</Button>}
    />
  );
}

Data that cannot be shown here

No tide prediction for this position

The tide tables don't cover these waters, so the depth shown is the charted depth only.
A tide panel where no tide tables cover the water: it says so, and what the depth shown is based on.
Code
import { EmptyState } from "@bosunkit/components/empty-state";
import type { ReactElement } from "react";

export const description: string =
  "A tide panel where no tide tables cover the water: it says so, and what the depth shown is based on.";

export default function Unavailable(): ReactElement {
  return (
    <EmptyState
      kind="unavailable"
      title="No tide prediction for this position"
      description="The tide tables don't cover these waters, so the depth shown is the charted depth only."
    />
  );
}

Nothing left to do

Every task is signed off

The next service, the engine's 250-hour, falls due in 38 engine hours.
The maintenance list with every task signed off: nothing left to do, in the success green.
Code
import { EmptyState } from "@bosunkit/components/empty-state";
import type { ReactElement } from "react";

export const description: string = "The maintenance list with every task signed off: nothing left to do, in the success green.";

export default function Done(): ReactElement {
  return (
    <EmptyState
      kind="done"
      title="Every task is signed off"
      description="The next service, the engine's 250-hour, falls due in 38 engine hours."
    />
  );
}

In a table

Voyage
From
To

No voyages yet

A voyage starts when the vessel leaves its berth.
A voyage table with no rows: the small state takes the table's empty content.
Code
import { DataTable, type Column } from "@bosunkit/components/data-table";
import { EmptyState } from "@bosunkit/components/empty-state";
import type { ReactElement } from "react";

export const description: string = "A voyage table with no rows: the small state takes the table's empty content.";

const COLUMNS: Column[] = [
  { key: "name", label: "Voyage" },
  { key: "from", label: "From" },
  { key: "to", label: "To" },
];

export default function InATable(): ReactElement {
  return (
    <DataTable
      columns={COLUMNS}
      rows={[]}
      empty={<EmptyState size="sm" title="No voyages yet" description="A voyage starts when the vessel leaves its berth." />}
    />
  );
}

Sizes

No attachments

No guests aboard

A guest is added from the crew page.

No passages planned

Plan one from the chart.
The three sizes: a list's cell, a panel, and a page with nothing else in it.
Code
import { EmptyState } from "@bosunkit/components/empty-state";
import type { ReactElement } from "react";

export const description: string = "The three sizes: a list's cell, a panel, and a page with nothing else in it.";

export default function Sizes(): ReactElement {
  return (
    <div style={{ display: "grid", gap: "var(--space-md)" }}>
      <EmptyState size="sm" align="start" title="No attachments" />
      <EmptyState size="md" title="No guests aboard" description="A guest is added from the crew page." />
      <EmptyState size="lg" title="No passages planned" description="Plan one from the chart." />
    </div>
  );
}

Props

PropTypeDefaultDescription
titlerequiredstring–What is empty, in one line, translated by the caller ("No checklist runs yet").
descriptionReactNode–Why it is empty or what happens next, translated by the caller ("The morning briefing is at 07:00 ship's time.").
kindEmptyStateKind | undefined"empty"Why there is nothing to show; empty unless set. It chooses the mark and whether the state is announced.
iconLucideIcon | ReactElement | null | undefined–Another mark: a Lucide icon, or an element such as an OpenBridge icon; null draws none. The kind's unless set.
actionsReactNode–The next step: one or two buttons or links, the first of them the main one ("Start a run").
headingLevelNestedHeadingLevel | undefined–Makes the title a heading at this level, for a state that stands for a section or a page; plain text unless set.
sizeEmptyStateSize | undefined"md"How much room it takes; md unless set.
align"center" | "start" | undefined"center"center for a panel or a page, start beside other content in a column; center unless set.
fillboolean | undefinedfalseGrows to the height of its container and centers itself in it, for a page or a panel with nothing else in it.
announceboolean | undefined–Reads the title out politely when the state appears (role="status"), for one that replaces results as the crew type or filter. On for no-results, off for the others, unless set.
childrenReactNode–More content under the description, such as a short list of what to try.
classNamestring | undefined–Classes added to the state's own.
testIdstring | undefined–Becomes data-testid on the root.
motionMotionProp | undefined–How the state moves. By default it fades in and out inside a MotionPresence, as results give way to it, and its new words fade in when the title changes. A preset or { enter, exit, change } chooses; false turns it off.
  • Section message: a message to one panel, such as a verdict on its data.
  • Data table: takes an empty state as its empty content.
  • Skeleton: what a panel shows while its data loads, before it knows it is empty.