docs / patterns / empty-state

EmptyState

a centered placeholder for a zero-data view: an icon, a message, and the one action that gets the user out of empty.

intensity · structuresrsc · server

provenance

authored in usva

layer

core / patterns

intensity

structures · organizes a region, stays out of the content

composition

fills the slot the data would occupy: a Panel body, a grid cell, a pagethe action is one Button, usually the way out of emptynever beside content, it marks the absence of itnot for failures. an error gets its own treatment, this is legitimately empty

a11y

the title is an h3 · the icon badge is aria-hiddenjest-axe
live demo · try it out

No projects yet

Create your first project to see it show up here.

customize
variantsolid surface or dashed outline
titlethe primary message
descriptionsupporting copy below the title
iconglyph badge above the title
actionthe way out of empty
actionLabeltext on the action button
usagecopy
import { EmptyState } from "usva/patterns/empty-state";
import { Button } from "usva/primitives/button";

<EmptyState
  icon={<FolderIcon />}
  title="No projects yet"
  description="Create your first project to see it show up here."
  action={<Button>New project</Button>}
/>

props

proptypedefaultnotes
iconReact.ReactNodeglyph in a rounded badge above the title. decorative.
titleReact.ReactNodethe primary message.
descriptionReact.ReactNodesupporting copy below the title.
actionReact.ReactNodethe way out of empty, rendered below the description.
variant"solid" | "dashed""solid"solid surface or a dashed transparent outline.

get it

npx shadcn add https://usva.build/r/empty-state.jsoncopy
source · components/ui/empty-state.tsxexactly what this command copiescopy
import * as React from "react";
import { cn } from "@/lib/utils";

export interface EmptyStateProps
  extends Omit<React.HTMLAttributes<HTMLDivElement>, "title"> {
  icon?: React.ReactNode;
  title: React.ReactNode;
  description?: React.ReactNode;
  action?: React.ReactNode;
  variant?: "solid" | "dashed";
}

export const EmptyState = React.forwardRef<HTMLDivElement, EmptyStateProps>(
  (
    {
      className,
      icon,
      title,
      description,
      action,
      variant = "solid",
      ...props
    },
    ref,
  ) => (
    <div
      ref={ref}
      className={cn(
        "group flex flex-col items-center justify-center rounded-2xl border border-border px-6 py-14 text-center",
        "transition-tint duration-slow ease-soft hover:border-border-strong",
        variant === "dashed" ? "border-dashed bg-transparent" : "bg-surface",
        className,
      )}
      {...props}
    >
      {icon != null && (
        <div
          aria-hidden="true"
          className={cn(
            "mb-4 flex h-11 w-11 items-center justify-center rounded-xl border border-border bg-surface-2 text-faint [&_svg]:h-5 [&_svg]:w-5",
            "transition-control duration-slow ease-soft",
            "group-hover:-translate-y-0.5 group-hover:border-border-strong group-hover:text-accent group-hover:glow-accent",
            "motion-reduce:transition-none motion-reduce:group-hover:translate-y-0",
          )}
        >
          {icon}
        </div>
      )}
      <h3 className="text-balance text-base font-semibold tracking-[-0.01em] text-ink">
        {title}
      </h3>
      {description != null && (
        <p className="mt-2 max-w-sm text-sm leading-relaxed text-pretty text-muted">
          {description}
        </p>
      )}
      {action != null && <div className="mt-6">{action}</div>}
    </div>
  ),
);
EmptyState.displayName = "EmptyState";