rapidUI

Empty

Empty state with icon, title, description and actions.

Installation

pnpm dlx shadcn@latest add https://rdf.dev.360online.com/r/empty.json

Installs to @/components/ui/empty.

Manual installation

Install the dependencies:

pnpm add class-variance-authority@^0.7.1

Copy the source into your project:

components/ui/empty.tsx
import { cva, type VariantProps } from 'class-variance-authority'
import { cn } from '@/lib/utils'

function Empty({ className, ...props }: React.ComponentProps<'div'>) {
	return (
		<div
			data-slot="empty"
			className={cn(
				'flex w-full min-w-0 flex-1 flex-col items-center justify-center gap-4 rounded-xl border-dashed p-6 text-center text-balance',
				className,
			)}
			{...props}
		/>
	)
}

function EmptyHeader({ className, ...props }: React.ComponentProps<'div'>) {
	return (
		<div data-slot="empty-header" className={cn('flex max-w-sm flex-col items-center gap-2', className)} {...props} />
	)
}

const emptyMediaVariants = cva(
	'mb-2 flex shrink-0 items-center justify-center [&_svg]:pointer-events-none [&_svg]:shrink-0',
	{
		variants: {
			variant: {
				default: 'bg-transparent',
				icon:
					"flex size-8 shrink-0 items-center justify-center rounded-lg bg-muted text-foreground [&_svg:not([class*='size-'])]:size-4",
			},
		},
		defaultVariants: {
			variant: 'default',
		},
	},
)

function EmptyMedia({
	className,
	variant = 'default',
	...props
}: React.ComponentProps<'div'> & VariantProps<typeof emptyMediaVariants>) {
	return (
		<div
			data-slot="empty-icon"
			data-variant={variant}
			className={cn(emptyMediaVariants({ variant, className }))}
			{...props}
		/>
	)
}

function EmptyTitle({
	className,
	as: Title = 'div',
	...props
}: React.ComponentProps<'div'> & {
	/** Render a heading (`h1`–`h6`) when the empty state needs a place in the page's heading outline, e.g. a 404 page */
	as?: 'div' | 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6'
}) {
	return (
		<Title
			data-slot="empty-title"
			className={cn('font-heading text-sm font-medium tracking-tight', className)}
			{...props}
		/>
	)
}

function EmptyDescription({ className, ...props }: React.ComponentProps<'p'>) {
	return (
		<div
			data-slot="empty-description"
			className={cn(
				'text-sm/relaxed text-muted-foreground [&>a]:underline [&>a]:underline-offset-4 [&>a:hover]:text-primary',
				className,
			)}
			{...props}
		/>
	)
}

function EmptyContent({ className, ...props }: React.ComponentProps<'div'>) {
	return (
		<div
			data-slot="empty-content"
			className={cn('flex w-full max-w-sm min-w-0 flex-col items-center gap-2.5 text-sm text-balance', className)}
			{...props}
		/>
	)
}

export { Empty, EmptyHeader, EmptyTitle, EmptyDescription, EmptyContent, EmptyMedia }

Displays an empty state, for example when a list has no results, an inbox is empty or a page cannot be found. It combines a header with media, title and description with optional content such as actions or a search field.

Features

  • Header with icon, avatar or custom media
  • Icon media variant with a muted background
  • Title and description with link styling
  • Content area for actions, inputs or links
  • Easy to outline or tint with utility classes

Usage

import { Button } from '@/components/ui/button'
import { Empty, EmptyContent, EmptyDescription, EmptyHeader, EmptyMedia, EmptyTitle } from '@/components/ui/empty'
import { FolderOpenIcon } from 'lucide-react'

export function NoCases() {
  return (
    <Empty>
      <EmptyHeader>
        <EmptyMedia variant="icon">
          <FolderOpenIcon />
        </EmptyMedia>
        <EmptyTitle>No cases yet</EmptyTitle>
        <EmptyDescription>Register a new case, or import one from the archive.</EmptyDescription>
      </EmptyHeader>
      <EmptyContent>
        <Button>Register case</Button>
      </EmptyContent>
    </Empty>
  )
}

Examples

Default

An empty case list with primary and secondary actions and a help link.

Outline

An outlined empty inbox. Add border to show the dashed outline.

Background

A tinted background (bg-muted/40) for an archive search without results.

With Avatar

Using an avatar as media when the case officer is away.

A not-found page with a search field.

In a Card

An empty state inside a Card, with smaller padding.

Accessibility

  • Media is decorative; keep the essential message in the title and description
  • Use a real heading element inside EmptyTitle via children if the empty state is a page section
  • Actions are regular buttons and links and keep their keyboard behaviour
  • Announce dynamically appearing empty states (e.g. after filtering) with a live region if needed