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.
How it's built
One `state` value drives which of the three bodies renders — Tabs is just a real, accessible way to switch it, not three separate routes.
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.
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 & pasteimport { 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>
);
}