Empty State

Shown where content would be, explaining why it is empty and what to do next.

No projects yet

Projects group your deployments and environments. Create one to get started.

Installation

Terminal
pnpm dlx @dowel-ui/cli add empty-state

npm packages installed: class-variance-authority.

Accessibility

A plain container with no implicit landmark or live region. When it replaces content after a search, put aria-live on the results region so the change is announced. The icon is decorative — the title has to carry the message on its own.

Props

EmptyState

PropTypeDefault
borderedboolean
size"sm" | "md" | "lg""md"

Plus every attribute of <div>.

EmptyStateIcon

Plus every attribute of <div>.

EmptyStateTitle

Plus every attribute of <p>.

EmptyStateDescription

Plus every attribute of <p>.

EmptyStateActions

Plus every attribute of <div>.

Quality

7/7 checks, measured from the source and its tests

  • Testedpasses
  • axe assertionpasses
  • Keyboard testeddoes not apply
  • Storybook examplespasses
  • Accessibility documentedpasses
  • Semantic tokens onlypasses
  • Motion from tokensdoes not apply
  • className mergedpasses
  • Visible focusdoes not apply
  • No fixed widthspasses

Used in

Whole screens assembled from this component. Installing one brings this and everything else it needs with it.

Source

This is exactly what dowel add empty-state writes into your project, with imports rewritten to your own path alias.

ui/empty-state.tsx
import { cva, type VariantProps } from "class-variance-authority";
import type { ComponentPropsWithRef } from "react";

import { cn } from "@/lib/utils";

const emptyStateVariants = cva("flex flex-col items-center justify-center gap-3 text-center", {
  variants: {
    size: {
      sm: "px-4 py-8",
      md: "px-6 py-12",
      lg: "px-8 py-20",
    },
    bordered: {
      true: "rounded-xl border border-dashed border-border",
      false: "",
    },
  },
  defaultVariants: {
    size: "md",
    bordered: false,
  },
});

export interface EmptyStateProps
  extends ComponentPropsWithRef<"div">, VariantProps<typeof emptyStateVariants> {}

/**
 * Shown where content would be, when there is none.
 *
 * An empty state should say why it is empty and what to do next. "No results"
 * after a search and "nothing here yet" on a new account are different
 * messages, and the second one is where a call to action belongs.
 */
export function EmptyState({ className, size, bordered, ...props }: EmptyStateProps) {
  return (
    <div
      data-slot="empty-state"
      className={cn(emptyStateVariants({ size, bordered }), className)}
      {...props}
    />
  );
}

/** Decorative by default — the title carries the meaning. */
export function EmptyStateIcon({ className, ...props }: ComponentPropsWithRef<"div">) {
  return (
    <div
      data-slot="empty-state-icon"
      aria-hidden="true"
      className={cn(
        "grid size-11 place-items-center rounded-full bg-muted text-muted-foreground",
        "[&_svg:not([class*='size-'])]:size-5",
        className,
      )}
      {...props}
    />
  );
}

export function EmptyStateTitle({ className, children, ...props }: ComponentPropsWithRef<"p">) {
  return (
    <p
      data-slot="empty-state-title"
      className={cn("text-base font-medium text-foreground", className)}
      {...props}
    >
      {children}
    </p>
  );
}

export function EmptyStateDescription({ className, ...props }: ComponentPropsWithRef<"p">) {
  return (
    <p
      data-slot="empty-state-description"
      className={cn("max-w-sm text-sm text-balance text-muted-foreground", className)}
      {...props}
    />
  );
}

export function EmptyStateActions({ className, ...props }: ComponentPropsWithRef<"div">) {
  return (
    <div
      data-slot="empty-state-actions"
      className={cn("mt-1 flex flex-wrap items-center justify-center gap-2", className)}
      {...props}
    />
  );
}

export { emptyStateVariants };