FairGarden Design

Empty State

What to show when a list, grid, search or collection has no items: what is empty, why, and what to do next.

import {
  EmptyState,
  EmptyStateAction,
  EmptyStateHeading,
  EmptyStateText,
} from '@fairgarden-private/design/components/EmptyState'

Render it in place of the list or grid. Pass role="status" when it replaces dynamic results. Text stays in the scope's full ink, never a faint gray. It is a sanctioned place for one ornament: a sticker drawing or a trail, never both. Don't use it for errors or loading.

Kinds

Kinds

framed (default) centers the message in a dashed frame; dashed means provisional, a place not yet filled. EmptyStateTrail draws the short-tail trail, a dashed line in the accent that ends in a triangle pointing down at the action. illustrated adds a sticker in EmptyStateDrawing (152 px, 64 px below 360 px of container): pass the art's viewBox, its halo path and its line art, and the Sticker draws the halo and line in its own light scope. From 1024 px of container the drawing sits beside the text, capped at 480 px. Use a drawing or a trail, never both. inline is one sentence in place of the content inside a dense panel, with no frame. Use an outline Button, or the solid one when it is the page's main next step.

No saved trails yet

Save a trail from its page and it will wait for you here.

No sightings this week

Be the first to log a bird on the preserve.

Readings will appear here after the first sync.

EmptyStateKinds.tsx
import { Button } from '@fairgarden-private/design/components/Button'
import {
  EmptyState,
  EmptyStateAction,
  EmptyStateDrawing,
  EmptyStateHeading,
  EmptyStateText,
  EmptyStateTrail,
} from '@fairgarden-private/design/components/EmptyState'
import { StickerDetail } from '@fairgarden-private/design/components/Sticker'
import styles from './kinds.module.css'

/** Framed (default) with the trail, illustrated with a sticker, and inline. */
export function EmptyStateKinds() {
  return (
    <div className={styles.stack}>
      <EmptyState>
        <EmptyStateTrail />
        <EmptyStateHeading>No saved trails yet</EmptyStateHeading>
        <EmptyStateText>Save a trail from its page and it will wait for you here.</EmptyStateText>
        <EmptyStateAction>
          <Button>Browse Trails</Button>
        </EmptyStateAction>
      </EmptyState>

      <EmptyState kind="illustrated">
        <EmptyStateDrawing
          viewBox="0 0 152 152"
          halo={
            <path d="M36 128c-8 0-8-16 0-16h4c-4-12-10-32-10-54 0-12 14-14 28-8 4-12 22-20 42-18h12c8 0 6 12 4 20-6 20-18 28-28 32v28h28c8 0 8 16 0 16z" />
          }
        >
          {/* A seedling: silhouette at the S weight, the leaf veins one step lighter. */}
          <path d="M76 120V64" />
          <path d="M76 84c-16 0-32-10-36-28 18-2 32 8 36 28z" />
          <path d="M76 72c14-2 28-14 32-30-18 0-30 12-32 30z" />
          <path d="M44 120h64" />
          <StickerDetail>
            <path d="M74 82 50 60M78 70l24-22" />
          </StickerDetail>
          <circle cx="76" cy="120" r="3" fill="currentColor" stroke="none" />
        </EmptyStateDrawing>
        <EmptyStateHeading>No sightings this week</EmptyStateHeading>
        <EmptyStateText>Be the first to log a bird on the preserve.</EmptyStateText>
        <EmptyStateAction>
          <Button variant="solid">Log a Sighting</Button>
        </EmptyStateAction>
      </EmptyState>

      <div className={styles.panel}>
        <EmptyState kind="inline">
          <EmptyStateText>Readings will appear here after the first sync.</EmptyStateText>
        </EmptyState>
      </div>
    </div>
  )
}

On paper and forest

On paper and forest

The frame, text and trail re-resolve on every ground; a sticker keeps its own light face. When a filter caused the empty state, set filtered: it becomes a polite status region and is left out of print. Name the filter and offer "Clear Filters".

paper

Nothing here yet

New events appear every Monday.

forest field

Nothing here yet

New events appear every Monday.

EmptyStateGrounds.tsx
import { Button } from '@fairgarden-private/design/components/Button'
import {
  EmptyState,
  EmptyStateAction,
  EmptyStateHeading,
  EmptyStateText,
} from '@fairgarden-private/design/components/EmptyState'
import { Ground } from '@fairgarden-private/design/components/Ground'
import styles from './grounds.module.css'

/** The dashed frame and text re-resolve on a page ground and inside a deep field. */
export function EmptyStateGrounds() {
  return (
    <div className={styles.row}>
      <Ground kind="face" preset="paper" className={styles.face}>
        <p className={styles.name}>paper</p>
        <Empty />
      </Ground>
      <Ground kind="field" preset="forest" className={styles.face}>
        <p className={styles.name}>forest field</p>
        <Empty />
      </Ground>
    </div>
  )
}

function Empty() {
  return (
    <EmptyState>
      <EmptyStateHeading>Nothing here yet</EmptyStateHeading>
      <EmptyStateText>New events appear every Monday.</EmptyStateText>
      <EmptyStateAction>
        <Button>See Past Events</Button>
      </EmptyStateAction>
    </EmptyState>
  )
}

API Reference

EmptyState

The empty state, rendered in place of a list or grid: an optional EmptyStateDrawing or EmptyStateTrail (one, never both), an EmptyStateHeading, an EmptyStateText and an EmptyStateAction (an outline Button, or the solid one when it is the page’s main next step). Pass role="status" when it replaces dynamic results (automatic with filtered). Never set its text lighter than the scope’s ink.

PropTypeDescription
filtered
boolean | undefined

A filter caused it: the root becomes a polite status region (role="status"), and it is not printed (print the unfiltered list, or “No items” once). Name the filter and offer “Clear Filters”. Default false.

kind
EmptyStateKind | undefined

framed (default): a centered stack in a line-dashed frame (dashed means provisional: a place not yet filled). inline: one sentence in place of the content, no frame, inside dense panels. illustrated: framed, plus a §6.5 sticker (S size) in EmptyStateDrawing.

primary
| 'ruby'
| 'olive'
| 'sage'
| 'slate'
| 'sand'
| 'gray'
| 'mauve'
| 'brown'
| 'bronze'
| 'gold'
| 'red'
| 'crimson'
| 'tomato'
| 'pink'
| 'plum'
| 'indigo'
| 'iris'
| 'violet'
| 'purple'
| null
| undefined

Primary Radix scale: the frame, heading and text. Never defaulted; omitted, it inherits the scope [D133].

secondary
| 'ruby'
| 'olive'
| 'sage'
| 'slate'
| 'sand'
| 'gray'
| 'mauve'
| 'brown'
| 'bronze'
| 'gold'
| 'red'
| 'crimson'
| 'tomato'
| 'pink'
| 'plum'
| 'indigo'
| 'iris'
| 'violet'
| 'purple'
| 'amber'
| 'blue'
| 'cyan'
| 'grass'
| 'green'
| 'jade'
| 'lime'
| 'mint'
| 'orange'
| 'sky'
| 'teal'
| 'yellow'
| null
| undefined

Secondary Radix scale: only the trail ornament (--role-accent). Never defaulted.

EmptyStateDrawing and EmptyStateTrail

The drawing (illustrated): a §6.5 Sticker at --ds-size-art-s with its halo, decorative (aria-hidden); the spot size --size-px-9 below 360 px. Pass the asset’s viewBox, its pre-expanded halo path and its line art as children. The sticker brings its own white scope, so it looks the same on every ground and ignores the empty state’s props.

PropTypeDescription
halo
React.ReactNode | undefined

The halo layer: the asset’s pre-expanded path, 8 px (--ds-space-halo) outside every part, merged into one silhouette with rounded concave bridges and no sharp outer corners. Never a runtime stroke, outline or blur. Filled --primary1; no edge, ever. Ignored with bare.

viewBox
string | undefined

The asset’s viewBox, drawn at its native size: 0 0 152 152 for S, 0 0 312 312 for L (other aspect ratios keep the width).

children
React.ReactNode | undefined

The line art, stroked in --role-heading at the silhouette weight (--ds-stroke-3 at S, --ds-stroke-4 at L), round caps and joins, no fill; give ink-spot parts fill="currentColor", and wrap interior lines in StickerDetail. No text inside the art.

The trail, the alternative to a drawing: the short-tail ornament-trail (utils/Ornament), from its origin circle to a marker-terminal that points down at the action, dashed in --role-accent, decorative. Hidden if it would touch type.

EmptyStateHeading, EmptyStateText and EmptyStateAction

What is empty: type-itemhead in --primary12, centered. Renders <h3>.

Why, and what to do next: type-body-ui in --primary12, at most 40ch. When a filter caused it, name the filter (“No events match ‘Virtual’").

The next step: an outline Button (md), or the solid one when it is the page’s main next step; “Clear Filters” after a filter (title case).

Additional types

emptyState
EmptyStateActionProps

Props for EmptyStateAction: div props and render.

type EmptyStateActionProps = useRender.ComponentProps<'div'>
EmptyStateDrawingProps

Props for EmptyStateDrawing: div props, render, and the sticker’s art (viewBox, halo, line art as children).

type EmptyStateDrawingProps = {
  /**
   * The line art, stroked in `--role-heading` at the silhouette weight
   * (`--ds-stroke-3` at S, `--ds-stroke-4` at L), round caps and joins, no
   * fill; give ink-spot parts `fill="currentColor"`, and wrap interior lines
   * in `StickerDetail`. No text inside the art.
   */
  children: React.ReactNode;
  /**
   * The asset's viewBox, drawn at its native size: `0 0 152 152` for S,
   * `0 0 312 312` for L (other aspect ratios keep the width).
   */
  viewBox: string;
  /**
   * The halo layer: the asset's pre-expanded path, 8 px (`--ds-space-halo`)
   * outside every part, merged into one silhouette with rounded concave
   * bridges and no sharp outer corners. Never a runtime stroke, outline or
   * blur. Filled `--primary1`; no edge, ever. Ignored with `bare`.
   */
  halo?: React.ReactNode;
}
EmptyStateHeadingProps

Props for EmptyStateHeading: heading props and render (default <h3>).

type EmptyStateHeadingProps = useRender.ComponentProps<'h3'>
EmptyStateKind

The three empty-state builds (§12.16).

type EmptyStateKind = 'framed' | 'inline' | 'illustrated'
EmptyStateProps

Props for EmptyState: div props, render, the kind, filtered and the color axes.

type EmptyStateProps = React.ClassAttributes<HTMLDivElement> &
  React.HTMLAttributes<HTMLDivElement> &
  __type & {
    kind?: EmptyStateKind;
    filtered?: boolean;
    primary?:
      | 'ruby'
      | 'olive'
      | 'sage'
      | 'slate'
      | 'sand'
      | 'gray'
      | 'mauve'
      | 'brown'
      | 'bronze'
      | 'gold'
      | 'red'
      | 'crimson'
      | 'tomato'
      | 'pink'
      | 'plum'
      | 'indigo'
      | 'iris'
      | 'violet'
      | 'purple'
      | null;
    secondary?:
      | 'ruby'
      | 'olive'
      | 'sage'
      | 'slate'
      | 'sand'
      | 'gray'
      | 'mauve'
      | 'brown'
      | 'bronze'
      | 'gold'
      | 'red'
      | 'crimson'
      | 'tomato'
      | 'pink'
      | 'plum'
      | 'indigo'
      | 'iris'
      | 'violet'
      | 'purple'
      | 'amber'
      | 'blue'
      | 'cyan'
      | 'grass'
      | 'green'
      | 'jade'
      | 'lime'
      | 'mint'
      | 'orange'
      | 'sky'
      | 'teal'
      | 'yellow'
      | null;
  }
EmptyStateTextProps

Props for EmptyStateText: paragraph props and render.

type EmptyStateTextProps = useRender.ComponentProps<'p'>
EmptyStateTrailProps

Props for EmptyStateTrail: div props and render; the trail itself is drawn.

type EmptyStateTrailProps = Omit<useRender.ComponentProps<'div', {}, HTMLProps>, 'children'>

Specification: DESIGN-SYSTEM.md §12.16 (empty state) and §5.10.2 (module reflow).