Skip to content
oks-ui

App screens

Empty & error states

The same persistent sidebar/header shell as the other App screens patterns, with a Tabs control that swaps the content card between the three states a real list screen has to handle: a Skeleton that mirrors the eventual row layout, an EmptyState with a call to action, and an inline Alert with retry — so the screen is never a confusing blank.

empty-and-error-states.tsx

How it's built

1

One `state` value drives which of the three bodies renders — Tabs is just a real, accessible way to switch it, not three separate routes.

2

The Skeleton rows mirror the shape of the content they'll be replaced by (an avatar circle plus two text lines) so the layout doesn't jump once real data arrives.

3

The Error Alert's Retry button flips `state` back to "loading" — a stand-in for the real refetch a production screen would trigger.

Source

One file · copy & paste
empty-and-error-states.tsx
import { useState } from "react";
import { Alert } from "oks-ui/alert";
import { Button } from "oks-ui/button";
import { EmptyState } from "oks-ui/empty-state";
import type { NavItemData } from "oks-ui/nav";
import { PageTitle } from "oks-ui/page-title";
import { Skeleton } from "oks-ui/skeleton";
import { Tab, Tabs } from "oks-ui/tabs";
import { PatternAppShell } from "../PatternAppShell";
import {
  BriefcaseIcon,
  CreditCardIcon,
  GridIcon,
  InboxIcon,
  PlusIcon,
  ReceiptIcon,
  SettingsIcon,
  UsersIcon,
} from "../../showcase/icons";

const NAV_ITEMS: NavItemData[] = [
  {
    key: "main",
    label: "Main menu",
    isSection: true,
    children: [
      { key: "dashboard", label: "Dashboard", icon: <GridIcon size={18} /> },
      { key: "projects", label: "Projects", icon: <BriefcaseIcon size={18} /> },
      { key: "invoices", label: "Invoices", icon: <ReceiptIcon size={18} /> },
      { key: "team", label: "Team", icon: <UsersIcon size={18} /> },
    ],
  },
  {
    key: "other",
    label: "Other",
    isSection: true,
    children: [
      { key: "billing", label: "Billing", icon: <CreditCardIcon size={18} /> },
      { key: "settings", label: "Settings", icon: <SettingsIcon size={18} /> },
    ],
  },
];

type ViewState = "loading" | "empty" | "error";

function ListRowSkeleton() {
  return (
    <div className="flex items-center gap-3 py-2.5">
      <Skeleton variant="circle" width={32} height={32} />
      <div className="flex flex-1 flex-col gap-1.5">
        <Skeleton variant="text" width="40%" />
        <Skeleton variant="text" width="65%" />
      </div>
    </div>
  );
}

export function EmptyAndErrorStates() {
  const [state, setState] = useState<ViewState>("loading");

  return (
    <PatternAppShell navItems={NAV_ITEMS} selectedNavKey="projects">
      <div className="flex shrink-0 flex-wrap items-center justify-between gap-3">
        <PageTitle
          as="h2"
          title="Projects"
          classNames={{ base: "flex-col items-start", title: "text-2xl font-bold tracking-tight text-ink" }}
        />
        <Button color="primary" radius="full" size="sm" startContent={<PlusIcon size={14} />}>
          New project
        </Button>
      </div>

      <Tabs
        aria-label="Preview a state"
        variant="underlined"
        selectedKey={state}
        onSelectionChange={(key) => setState(key as ViewState)}
        classNames={{ base: "shrink-0" }}
      >
        <Tab key="loading" title="Loading" />
        <Tab key="empty" title="Empty" />
        <Tab key="error" title="Error" />
      </Tabs>

      <div className="flex flex-1 flex-col rounded-[var(--r-lg)] bg-paper-raised p-6 shadow-[var(--shadow-sm)]">
        {state === "loading" ? (
          <div className="flex flex-col divide-y divide-line">
            <ListRowSkeleton />
            <ListRowSkeleton />
            <ListRowSkeleton />
          </div>
        ) : state === "empty" ? (
          <div className="flex flex-1 items-center justify-center">
            <EmptyState
              icon={<InboxIcon />}
              title="No projects yet"
              description="Create your first project to start tracking work with your team."
              actions={
                <Button color="primary" radius="full" startContent={<PlusIcon size={14} />}>
                  New project
                </Button>
              }
            />
          </div>
        ) : (
          <Alert
            color="danger"
            title="Couldn't load projects"
            description="Something went wrong on our end. Try again, or contact support if the problem continues."
            actions={
              <Button size="sm" variant="soft" color="danger" onClick={() => setState("loading")}>
                Retry
              </Button>
            }
          />
        )}
      </div>
    </PatternAppShell>
  );
}

More patterns

App screens
Analytics dashboardA full fintech app screen — collapsible sidebar, header search, wallet cards, an earnings chart, savings goals, and a transactions table.
App screens
Settings panelA real settings screen — top tabs, two-column sections, and a sortable, selectable billing history table.
App screens
Data table viewA full documents list screen — status Tabs, a filter toolbar, and a sortable, selectable Table with real recipient avatars.