Docs
Empty states

Empty states

Convey the absence of data and provide clear instruction for what to do about it.

Empty states convey the fact that there is nothing to list, perform, or display on the current page. Ideally, they also provide a clear action for the user to take.

Best practices

No data

There are two ways an empty state may be displayed in cases where there is no data:

  • Initial state: no data to begin with
  • Zero results: no data after a search or filter

Initial state

Perhaps the user has not yet created any data. The presentation of this empty state depends on the context of the list and the type of data it contains. Be mindful of the journey to rendering an empty state and any possible layout shift along the way.

Presentational

The user may be learning about a feature for the first time, and could benefit from lightweight feature education or onboarding. Use the dedicated Empty State Presentational component in this case, putting emphasis on an action the user can take.

Loading...
import { BucketPlus } from 'icons'
import { Plus } from 'lucide-react'
import { Button } from 'ui'
import { EmptyStatePresentational } from 'ui-patterns'
 
export function EmptyStatePresentationalIcon() {
  return (
    <EmptyStatePresentational
      icon={BucketPlus}
      title="Create a vector bucket"
      description="Store, index, and query your vector embeddings at scale."
    >
      <Button size="tiny" type="primary" icon={<Plus size={14} />}>
        Create bucket
      </Button>
    </EmptyStatePresentational>
  )
}

Remember to use active language in presentational empty states. For example: “Create a vector bucket” instead of “No vector buckets found”. The latter is more appropriate in table-based presentations, as described below.

Informational

Or perhaps the list type is data-heavy or does not benefit from additional information. In these cases, the empty state should provide show the initial state in the same presentation as the list when there is data, much like the zero results scenario.

Loading...
import { Plus } from 'lucide-react'
import { Button, Card, Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from 'ui'
 
export function EmptyStateZeroItemsTable() {
  return (
    <div className="flex flex-col gap-y-4 w-full">
      <Button className="w-fit self-end" type="primary" size="tiny" icon={<Plus size={14} />}>
        New table
      </Button>
      <Card>
        <Table>
          <TableHeader>
            <TableRow>
              <TableHead className="text-foreground-muted">Table name</TableHead>
              <TableHead className="text-foreground-muted">Date created</TableHead>
              <TableHead />
            </TableRow>
          </TableHeader>
          <TableBody>
            <TableRow className="[&>td]:hover:bg-inherit">
              <TableCell colSpan={3}>
                <p className="text-sm text-foreground">No tables yet</p>
                <p className="text-sm text-foreground-lighter">Create a table to get started</p>
              </TableCell>
            </TableRow>
          </TableBody>
        </Table>
      </Card>
    </div>
  )
}

Keep in mind that empty states will likely appear after a visual loading state. Consider layout shift and button placement during and after the transition.

Zero results

Data-heavy presentations without results should have an empty state that broadly matches the state when there is data. This makes the transition between the two states more seamless.

Table

A Table instance with zero results should display a single row. Dulling the TableHead text color and removing the TableCell hover state can further reinforce the lack of usable data.

Loading...
import { Card, Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from 'ui'
 
export function EmptyStateZeroItemsTable() {
  return (
    <Card className="w-full">
      <Table>
        <TableHeader>
          <TableRow>
            <TableHead className="text-foreground-muted">Name</TableHead>
            <TableHead className="text-foreground-muted">Created</TableHead>
            <TableHead className="text-foreground-muted">Updated</TableHead>
          </TableRow>
        </TableHeader>
        <TableBody>
          <TableRow className="[&>td]:hover:bg-inherit">
            <TableCell colSpan={3}>
              <p className="text-sm text-foreground">No results found</p>
              <p className="text-sm text-foreground-lighter">
                Your search for “test” did not return any results
              </p>
            </TableCell>
          </TableRow>
        </TableBody>
      </Table>
    </Card>
  )
}

Studio contains two pre-built components to handle these cases consistently:

  • No Filter Results
  • No Search Results
Data Grid

Data Grid and Data Table component patterns typically span the full height and width of a container. A classic example is Users, which (as it sounds) displays a list of the project’s registered users. Any instance with zero results should display a more prominent empty with a clear title, description, and supporting illustration.

Loading...
import { Users } from 'lucide-react'
import DataGrid, { Column } from 'react-data-grid'
import 'react-data-grid/lib/styles.css'
import { cn } from 'ui'
 
/**
 * Renders a DataGrid configured to display an empty "no users" state.
 *
 * The grid includes three columns (Display name, Email, Phone) and no rows, and supplies a centered overlay with an icon and explanatory text when there are no rows.
 *
 * @returns A React element containing the configured DataGrid with an empty-state fallback UI.
 */
export function DataGridEmptyState() {
  const columns: Column<{ id: string; name: string; email: string }>[] = [
    { key: 'name', name: 'Display name', minWidth: 200, resizable: true },
    { key: 'email', name: 'Email', minWidth: 250, resizable: true },
    { key: 'phone', name: 'Phone', minWidth: 150, resizable: true },
  ]
 
  const rows: { id: string; name: string; email: string }[] = []
 
  return (
    <div className="h-full w-full flex flex-col relative min-h-[400px] rounded-md border overflow-hidden">
      <DataGrid
        className="flex-grow border-t-0 bg-dash-canvas"
        rowHeight={44}
        headerRowHeight={36}
        columns={columns}
        rows={rows}
        rowKeyGetter={(row: { id: string; name: string; email: string }) => row.id}
        rowClass={() => {
          return cn(
            'bg-surface-200 cursor-pointer',
            '[&>.rdg-cell]:border-box [&>.rdg-cell]:outline-none [&>.rdg-cell]:shadow-none',
            '[&>.rdg-cell:first-child>div]:ml-8'
          )
        }}
        renderers={{
          noRowsFallback: (
            <div className="absolute top-20 px-6 flex flex-col items-center justify-center w-full gap-y-2">
              <Users className="text-foreground-lighter" strokeWidth={1} />
              <div className="text-center">
                <p className="text-foreground">No users in your project</p>
                <p className="text-foreground-light">
                  There are currently no users who signed up to your project
                </p>
              </div>
            </div>
          ),
        }}
      />
    </div>
  )
}

Other Data Grid instances include Cron Jobs and Queues.

Missing route

Users may accidentally navigate to a non-existent dynamic route, such as a non-existent bucket in Storage or a non-existent table in the Table Editor. In these cases, follow the pattern of a centered Admonition as shown below.

Loading...
import Link from 'next/link'
import { Button } from 'ui'
import { Admonition } from 'ui-patterns/admonition'
 
const bucketId = 'user_avatars'
 
export function EmptyStateMissingRoute() {
  return (
    <div className="flex items-center justify-center w-full h-full">
      <Admonition
        type="default"
        className="max-w-md"
        title="Unable to find bucket"
        description={`${bucketId ? `The bucket “${bucketId}` : 'This bucket'} doesn’t seem to exist.`}
      >
        <Button asChild type="default" className="mt-2">
          <Link
            href="/"
            onClick={(e) => {
              e.preventDefault() // Just for demo purposes
            }}
          >
            Head back
          </Link>
        </Button>
      </Admonition>
    </div>
  )
}

Components

For presentational empty states (initial states with value propositions and actions), use the Empty State Presentational component from ui-patterns. This component provides a consistent structure with support for icons, titles, descriptions, and action buttons.

For other empty state scenarios (zero results, missing routes, etc), custom components may still be appropriate as the context and needs for each placement can differ significantly.