Empty State

Centered placeholder for empty lists, tables, and search results with sm/md/lg sizes.

Report a bug

Preview

Switch between light and dark to inspect the embedded Storybook preview.

Installation

pnpm dlx shadcn@latest add https://ui.vllnt.ai/r/empty-state.json
bash

Storybook

Explore all variants, controls, and accessibility checks in the interactive Storybook playground.

View in Storybook

Code

import {
  type ComponentPropsWithoutRef,
  forwardRef,
  type ReactNode,
} from "react";

import { cva, type VariantProps } from "class-variance-authority";

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

const emptyStateVariants = cva(
  "flex flex-col items-center justify-center gap-3 text-center text-foreground",
  {
    defaultVariants: {
      size: "md",
    },
    variants: {
      size: {
        lg: "px-6 py-16",
        md: "px-4 py-10",
        sm: "px-3 py-6",
      },
    },
  },
);

const titleVariants = cva("font-semibold tracking-tight", {
  defaultVariants: {
    size: "md",
  },
  variants: {
    size: {
      lg: "text-xl",
      md: "text-lg",
      sm: "text-base",
    },
  },
});

const descriptionVariants = cva("max-w-prose text-muted-foreground", {
  defaultVariants: {
    size: "md",
  },
  variants: {
    size: {
      lg: "text-base",
      md: "text-sm",
      sm: "text-xs",
    },
  },
});

const iconVariants = cva(
  "mb-1 flex items-center justify-center rounded-full bg-muted text-muted-foreground [&_svg]:h-1/2 [&_svg]:w-1/2",
  {
    defaultVariants: {
      size: "md",
    },
    variants: {
      size: {
        lg: "size-16",
        md: "size-12",
        sm: "size-8",
      },
    },
  },
);

/**
 * Visual size for {@link EmptyState}.
 *
 * - `sm` — inline, suitable for compact lists or cards
 * - `md` — section-level (default)
 * - `lg` — full-page placeholder
 *
 * @public
 */
export type EmptyStateSize = "lg" | "md" | "sm";

/**
 * Props for {@link EmptyState}.
 *
 * @public
 */
export type EmptyStateProps = {
  /**
   * Action slot rendered below the description. Compose with `Button` or any
   * interactive element.
   */
  children?: ReactNode;
  /** Optional secondary text describing the empty condition. */
  description?: ReactNode;
  /** Optional icon or illustration shown above the title. */
  icon?: ReactNode;
  /** Headline rendered as a heading element. */
  title?: ReactNode;
} & ComponentPropsWithoutRef<"div"> &
  VariantProps<typeof emptyStateVariants>;

/**
 * Placeholder for empty lists, tables, and search results. Composes a
 * centered icon, title, description, and an action slot. Announces itself
 * via `role="status"` so assistive tech can pick up state changes
 * (e.g. when filters clear results).
 *
 * @example
 * ```tsx
 * <EmptyState
 *   icon={<Inbox />}
 *   title="No results found"
 *   description="Try adjusting your search or filters."
 * >
 *   <Button variant="outline" onClick={clearFilters}>Clear filters</Button>
 * </EmptyState>
 * ```
 *
 * @public
 */
export const EmptyState = forwardRef<HTMLDivElement, EmptyStateProps>(
  (
    {
      children,
      className,
      description,
      icon,
      role: roleOverride,
      size,
      title,
      ...rest
    },
    ref,
  ) => {
    return (
      <div
        className={cn(emptyStateVariants({ size }), className)}
        ref={ref}
        role={roleOverride ?? "status"}
        {...rest}
      >
        {icon ? (
          <span aria-hidden="true" className={cn(iconVariants({ size }))}>
            {icon}
          </span>
        ) : null}
        {title ? (
          <h3 className={cn(titleVariants({ size }))}>{title}</h3>
        ) : null}
        {description ? (
          <p className={cn(descriptionVariants({ size }))}>{description}</p>
        ) : null}
        {children ? (
          <div className="mt-2 flex flex-wrap items-center justify-center gap-2">
            {children}
          </div>
        ) : null}
      </div>
    );
  },
);
EmptyState.displayName = "EmptyState";

export { emptyStateVariants };
typescript

Dependencies

  • @vllnt/ui@^0.2.1
  • class-variance-authority