Empty
Free, copy-and-go Empty components built on the SevenUI Empty primitive.Read the primitive docs.
No saved replies
Canned responses you save from the composer show up here. Learn how to save one
"use client";
import {
Empty,
EmptyDescription,
EmptyHeader,
EmptyTitle,
} from "@/components/ui/empty";
export default function Empty01() {
return (
<Empty className="w-full max-w-sm p-4">
<EmptyHeader>
<EmptyTitle>No saved replies</EmptyTitle>
<EmptyDescription>
Canned responses you save from the composer show up here.{" "}
<a href="#saved-replies">Learn how to save one</a>
</EmptyDescription>
</EmptyHeader>
</Empty>
);
}
npx shadcn@latest add @sevenui/component/empty-01pnpm dlx shadcn@latest add @sevenui/component/empty-01yarn dlx shadcn@latest add @sevenui/component/empty-01bunx --bun shadcn@latest add @sevenui/component/empty-01No collections yet
Group related documents into a collection so your team can find them in one place.
"use client";
import { FolderPlusIcon, UploadIcon } from "lucide-react";
import { Button } from "@/components/ui/button";
import {
Empty,
EmptyContent,
EmptyDescription,
EmptyHeader,
EmptyMedia,
EmptyTitle,
} from "@/components/ui/empty";
export default function Empty02() {
return (
<Empty className="w-full max-w-md border md:p-10">
<EmptyHeader>
<EmptyMedia variant="icon">
<FolderPlusIcon aria-hidden="true" />
</EmptyMedia>
<EmptyTitle>No collections yet</EmptyTitle>
<EmptyDescription>
Group related documents into a collection so your team can find them
in one place.
</EmptyDescription>
</EmptyHeader>
<EmptyContent>
<div className="flex flex-wrap justify-center gap-2">
<Button size="sm">
<FolderPlusIcon aria-hidden="true" data-icon="inline-start" />
New collection
</Button>
<Button size="sm" variant="outline">
<UploadIcon aria-hidden="true" data-icon="inline-start" />
Import files
</Button>
</div>
</EmptyContent>
</Empty>
);
}
npx shadcn@latest add @sevenui/component/empty-02pnpm dlx shadcn@latest add @sevenui/component/empty-02yarn dlx shadcn@latest add @sevenui/component/empty-02bunx --bun shadcn@latest add @sevenui/component/empty-02No webhooks configured
Send order and refund events to your own endpoint in real time.
"use client";
import { WebhookIcon } from "lucide-react";
import { Button } from "@/components/ui/button";
import {
Empty,
EmptyContent,
EmptyDescription,
EmptyHeader,
EmptyMedia,
EmptyTitle,
} from "@/components/ui/empty";
export default function Empty03() {
return (
<Empty className="w-full max-w-xl flex-col items-start gap-4 border p-4 text-left sm:flex-row sm:items-center sm:gap-4">
<EmptyMedia variant="icon" className="mb-0 size-10">
<WebhookIcon aria-hidden="true" />
</EmptyMedia>
<EmptyHeader className="max-w-none flex-1 items-start gap-1">
<EmptyTitle>No webhooks configured</EmptyTitle>
<EmptyDescription>
Send order and refund events to your own endpoint in real time.
</EmptyDescription>
</EmptyHeader>
<EmptyContent className="w-auto max-w-none items-start">
<Button size="sm" variant="outline">
Add endpoint
</Button>
</EmptyContent>
</Empty>
);
}
npx shadcn@latest add @sevenui/component/empty-03pnpm dlx shadcn@latest add @sevenui/component/empty-03yarn dlx shadcn@latest add @sevenui/component/empty-03bunx --bun shadcn@latest add @sevenui/component/empty-03Build your first dashboard
Pin charts from any report to track signups, revenue, and churn on one screen your whole team can open.
"use client";
import { ChartColumnIcon, ChartPieIcon, LayoutDashboardIcon } from "lucide-react";
import { Button } from "@/components/ui/button";
import {
Empty,
EmptyContent,
EmptyDescription,
EmptyHeader,
EmptyMedia,
EmptyTitle,
} from "@/components/ui/empty";
export default function Empty04() {
return (
<Empty className="w-full max-w-md bg-muted/60 p-8 md:p-12">
<EmptyHeader>
<EmptyMedia className="mb-4">
<div
aria-hidden="true"
className="relative flex h-14 w-24 items-end justify-center"
>
<div className="absolute bottom-1 left-1 flex size-10 -rotate-12 items-center justify-center rounded-xl border bg-card text-muted-foreground shadow-sm">
<ChartColumnIcon className="size-4" />
</div>
<div className="absolute right-1 bottom-1 flex size-10 rotate-12 items-center justify-center rounded-xl border bg-card text-muted-foreground shadow-sm">
<ChartPieIcon className="size-4" />
</div>
<div className="relative flex size-12 items-center justify-center rounded-xl border bg-card text-foreground shadow-md">
<LayoutDashboardIcon className="size-5" />
</div>
</div>
</EmptyMedia>
<EmptyTitle className="text-base">Build your first dashboard</EmptyTitle>
<EmptyDescription>
Pin charts from any report to track signups, revenue, and churn on
one screen your whole team can open.
</EmptyDescription>
</EmptyHeader>
<EmptyContent>
<div className="flex flex-wrap justify-center gap-2">
<Button>Create dashboard</Button>
<Button variant="ghost">Start from a template</Button>
</div>
</EmptyContent>
</Empty>
);
}
npx shadcn@latest add @sevenui/component/empty-04pnpm dlx shadcn@latest add @sevenui/component/empty-04yarn dlx shadcn@latest add @sevenui/component/empty-04bunx --bun shadcn@latest add @sevenui/component/empty-04"use client";
import { CreditCardIcon, PlusIcon } from "lucide-react";
import { Button } from "@/components/ui/button";
import {
Card,
CardAction,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import {
Empty,
EmptyDescription,
EmptyHeader,
EmptyMedia,
EmptyTitle,
} from "@/components/ui/empty";
export default function Empty05() {
return (
<Card className="w-full max-w-md">
<CardHeader>
<CardTitle>Payment methods</CardTitle>
<CardDescription>Used for your Pro plan renewals.</CardDescription>
<CardAction>
<Button size="sm" variant="outline">
<PlusIcon aria-hidden="true" data-icon="inline-start" />
Add card
</Button>
</CardAction>
</CardHeader>
<CardContent>
<Empty className="gap-2 border bg-muted/30 p-4">
<EmptyHeader className="gap-1">
<EmptyMedia variant="icon" className="mb-1 size-7">
<CreditCardIcon aria-hidden="true" className="size-3.5" />
</EmptyMedia>
<EmptyTitle className="text-[0.8rem]">No card on file</EmptyTitle>
<EmptyDescription className="text-xs/relaxed">
Your trial ends on October 12. Add a card to keep your projects
online after that.
</EmptyDescription>
</EmptyHeader>
</Empty>
</CardContent>
</Card>
);
}
npx shadcn@latest add @sevenui/component/empty-05pnpm dlx shadcn@latest add @sevenui/component/empty-05yarn dlx shadcn@latest add @sevenui/component/empty-05bunx --bun shadcn@latest add @sevenui/component/empty-05No reviewers on this pull request
Based on CODEOWNERS, Priya, Marcus, and Lena usually review changes to billing/.
"use client";
import * as React from "react";
import { CheckIcon, UserPlusIcon } from "lucide-react";
import {
Avatar,
AvatarFallback,
AvatarGroup,
AvatarGroupCount,
} from "@/components/ui/avatar";
import { Button } from "@/components/ui/button";
import {
Empty,
EmptyContent,
EmptyDescription,
EmptyHeader,
EmptyMedia,
EmptyTitle,
} from "@/components/ui/empty";
const suggestedReviewers = [
{ initials: "PR", name: "Priya Raman" },
{ initials: "MO", name: "Marcus Osei" },
{ initials: "LV", name: "Lena Vogel" },
];
export default function Empty06() {
const [requested, setRequested] = React.useState(false);
return (
<Empty className="w-full max-w-md border">
<EmptyHeader>
<EmptyMedia>
<AvatarGroup aria-hidden="true">
{suggestedReviewers.map((reviewer) => (
<Avatar
key={reviewer.initials}
size="lg"
className={requested ? undefined : "opacity-60"}
>
<AvatarFallback className="text-xs">
{reviewer.initials}
</AvatarFallback>
</Avatar>
))}
<AvatarGroupCount className="border border-dashed border-muted-foreground/40 bg-background">
{requested ? <CheckIcon /> : <UserPlusIcon />}
</AvatarGroupCount>
</AvatarGroup>
</EmptyMedia>
<EmptyTitle>
{requested ? "Review requested" : "No reviewers on this pull request"}
</EmptyTitle>
<EmptyDescription>
{requested
? "Priya, Marcus, and Lena were notified. Merging unlocks after one approval."
: "Based on CODEOWNERS, Priya, Marcus, and Lena usually review changes to billing/."}
</EmptyDescription>
</EmptyHeader>
<EmptyContent>
<div className="flex flex-wrap justify-center gap-2">
<Button
size="sm"
disabled={requested}
onClick={() => setRequested(true)}
>
{requested ? "Requested" : "Request all 3"}
</Button>
<Button
size="sm"
variant="outline"
onClick={() => setRequested(false)}
>
{requested ? "Undo request" : "Choose reviewers"}
</Button>
</div>
<p aria-live="polite" className="sr-only">
{requested ? "Review requested from 3 code owners." : ""}
</p>
</EmptyContent>
</Empty>
);
}
npx shadcn@latest add @sevenui/component/empty-06pnpm dlx shadcn@latest add @sevenui/component/empty-06yarn dlx shadcn@latest add @sevenui/component/empty-06bunx --bun shadcn@latest add @sevenui/component/empty-06View only
No reports in this workspace
You have viewer access to Finance. Editors can create reports and share them with you.
"use client";
import * as React from "react";
import { CheckCircle2Icon, LockIcon, PlusIcon } from "lucide-react";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import {
Empty,
EmptyContent,
EmptyDescription,
EmptyHeader,
EmptyMedia,
EmptyTitle,
} from "@/components/ui/empty";
export default function Empty07() {
const [requested, setRequested] = React.useState(false);
return (
<Empty className="w-full max-w-md border">
<EmptyHeader>
<EmptyMedia variant="icon" className="size-10">
<LockIcon aria-hidden="true" />
</EmptyMedia>
<Badge variant="outline">View only</Badge>
<EmptyTitle>No reports in this workspace</EmptyTitle>
<EmptyDescription id="empty-07-reason">
You have viewer access to Finance. Editors can create reports and
share them with you.
</EmptyDescription>
</EmptyHeader>
<EmptyContent>
<div className="flex flex-wrap justify-center gap-2">
<Button size="sm" disabled aria-describedby="empty-07-reason">
<PlusIcon aria-hidden="true" data-icon="inline-start" />
Create report
</Button>
<Button
size="sm"
variant="outline"
disabled={requested}
onClick={() => setRequested(true)}
>
{requested ? "Request sent" : "Request edit access"}
</Button>
</div>
<div aria-live="polite" className="min-h-5">
{requested ? (
<p className="flex items-center gap-1.5 text-xs text-muted-foreground">
<CheckCircle2Icon aria-hidden="true" className="size-3.5 text-success" />
Maya Chen, the workspace owner, has been notified.
</p>
) : null}
</div>
</EmptyContent>
</Empty>
);
}
npx shadcn@latest add @sevenui/component/empty-07pnpm dlx shadcn@latest add @sevenui/component/empty-07yarn dlx shadcn@latest add @sevenui/component/empty-07bunx --bun shadcn@latest add @sevenui/component/empty-07Couldn't load invoices
The billing service didn't respond. Your data is safe; try again in a moment.
"use client";
import * as React from "react";
import { CloudOffIcon, InboxIcon, RotateCwIcon } from "lucide-react";
import { Button } from "@/components/ui/button";
import {
Empty,
EmptyContent,
EmptyDescription,
EmptyHeader,
EmptyMedia,
EmptyTitle,
} from "@/components/ui/empty";
import { Spinner } from "@/components/ui/spinner";
type Status = "error" | "retrying" | "resolved";
// The first retry fails again; the second one reaches the server.
const attemptsUntilSuccess = 2;
export default function Empty08() {
const [status, setStatus] = React.useState<Status>("error");
const [attempts, setAttempts] = React.useState(0);
React.useEffect(() => {
if (status !== "retrying") return;
const timeout = window.setTimeout(() => {
setStatus(attempts >= attemptsUntilSuccess ? "resolved" : "error");
}, 1200);
return () => window.clearTimeout(timeout);
}, [status, attempts]);
const retry = () => {
setAttempts((count) => count + 1);
setStatus("retrying");
};
const reset = () => {
setAttempts(0);
setStatus("error");
};
if (status === "resolved") {
return (
<Empty className="w-full max-w-md border" aria-live="polite">
<EmptyHeader>
<EmptyMedia variant="icon" className="size-10">
<InboxIcon aria-hidden="true" />
</EmptyMedia>
<EmptyTitle>Connected, no invoices yet</EmptyTitle>
<EmptyDescription>
Invoices from Stripe will sync here as soon as the first one is
issued.
</EmptyDescription>
</EmptyHeader>
<EmptyContent>
<Button size="sm" variant="ghost" onClick={reset}>
Replay the error state
</Button>
</EmptyContent>
</Empty>
);
}
const retrying = status === "retrying";
return (
<Empty
className="w-full max-w-md border border-destructive/30 bg-destructive/5"
aria-live="polite"
>
<EmptyHeader>
<EmptyMedia
variant="icon"
className="size-10 bg-destructive/10 text-destructive"
>
<CloudOffIcon aria-hidden="true" />
</EmptyMedia>
<EmptyTitle>Couldn't load invoices</EmptyTitle>
<EmptyDescription>
{attempts === 0
? "The billing service didn't respond. Your data is safe; try again in a moment."
: `Still unreachable after ${attempts} ${attempts === 1 ? "retry" : "retries"}. One more try usually does it.`}
</EmptyDescription>
</EmptyHeader>
<EmptyContent>
<div className="flex flex-wrap justify-center gap-2">
<Button size="sm" variant="outline" disabled={retrying} onClick={retry}>
{retrying ? (
<Spinner data-icon="inline-start" aria-hidden="true" />
) : (
<RotateCwIcon aria-hidden="true" data-icon="inline-start" />
)}
{retrying ? "Retrying" : "Try again"}
</Button>
<Button size="sm" variant="ghost" nativeButton={false} render={<a href="#status" />}>
View status page
</Button>
</div>
</EmptyContent>
</Empty>
);
}
npx shadcn@latest add @sevenui/component/empty-08pnpm dlx shadcn@latest add @sevenui/component/empty-08yarn dlx shadcn@latest add @sevenui/component/empty-08bunx --bun shadcn@latest add @sevenui/component/empty-08Scheduled posts
Sep 28 – Oct 4
"use client";
import * as React from "react";
import { CalendarClockIcon, PlusIcon, RefreshCwIcon } from "lucide-react";
import { Button } from "@/components/ui/button";
import {
Empty,
EmptyContent,
EmptyDescription,
EmptyHeader,
EmptyMedia,
EmptyTitle,
} from "@/components/ui/empty";
import { Skeleton } from "@/components/ui/skeleton";
const skeletonRows = ["w-3/4", "w-1/2", "w-2/3"];
export default function Empty09() {
const [loading, setLoading] = React.useState(true);
const [reloads, setReloads] = React.useState(0);
// biome-ignore lint/correctness/useExhaustiveDependencies: reloads re-runs the simulated fetch
React.useEffect(() => {
setLoading(true);
const timeout = window.setTimeout(() => setLoading(false), 1400);
return () => window.clearTimeout(timeout);
}, [reloads]);
return (
<div className="w-full max-w-md rounded-xl border bg-card">
<div className="flex items-center justify-between gap-2 border-b px-4 py-3">
<div className="min-w-0">
<h3 className="text-sm font-medium">Scheduled posts</h3>
<p className="text-xs text-muted-foreground">Sep 28 – Oct 4</p>
</div>
<Button
size="icon-sm"
variant="ghost"
aria-label="Refresh scheduled posts"
disabled={loading}
onClick={() => setReloads((count) => count + 1)}
>
<RefreshCwIcon
aria-hidden="true"
className={loading ? "animate-spin motion-reduce:animate-none" : undefined}
/>
</Button>
</div>
<div aria-busy={loading} aria-live="polite" className="min-h-56">
{loading ? (
<ul aria-label="Loading scheduled posts" className="grid gap-4 p-4">
{skeletonRows.map((width) => (
<li key={width} className="flex items-center gap-3">
<Skeleton className="size-10 shrink-0 rounded-md" />
<div className="grid flex-1 gap-2">
<Skeleton className={`h-3 ${width}`} />
<Skeleton className="h-3 w-1/3" />
</div>
</li>
))}
</ul>
) : (
<Empty className="min-h-56 transition-[opacity,translate] duration-300 ease-out starting:translate-y-1 starting:opacity-0 motion-reduce:transition-none">
<EmptyHeader>
<EmptyMedia variant="icon">
<CalendarClockIcon aria-hidden="true" />
</EmptyMedia>
<EmptyTitle>Nothing scheduled next week</EmptyTitle>
<EmptyDescription>
Your queue runs dry on Monday. Accounts that post at least three times a week keep their reach steady.
</EmptyDescription>
</EmptyHeader>
<EmptyContent>
<Button size="sm">
<PlusIcon aria-hidden="true" data-icon="inline-start" />
Schedule a post
</Button>
</EmptyContent>
</Empty>
)}
</div>
</div>
);
}
npx shadcn@latest add @sevenui/component/empty-09pnpm dlx shadcn@latest add @sevenui/component/empty-09yarn dlx shadcn@latest add @sevenui/component/empty-09bunx --bun shadcn@latest add @sevenui/component/empty-090 results
No integrations match “Notion”
Check the spelling, or try one of the popular ones below.
Press Esc to clear the search
"use client";
import * as React from "react";
import { SearchIcon, SearchXIcon, XIcon } from "lucide-react";
import { Button } from "@/components/ui/button";
import {
Empty,
EmptyContent,
EmptyDescription,
EmptyHeader,
EmptyMedia,
EmptyTitle,
} from "@/components/ui/empty";
import {
InputGroup,
InputGroupAddon,
InputGroupButton,
InputGroupInput,
} from "@/components/ui/input-group";
import { Kbd } from "@/components/ui/kbd";
const integrations = [
{ name: "GitHub", category: "Source control" },
{ name: "GitLab", category: "Source control" },
{ name: "Linear", category: "Issue tracking" },
{ name: "Jira", category: "Issue tracking" },
{ name: "Slack", category: "Messaging" },
{ name: "Figma", category: "Design" },
];
const suggestions = ["Slack", "Linear", "Figma"];
export default function Empty10() {
const [query, setQuery] = React.useState("Notion");
const inputRef = React.useRef<HTMLInputElement>(null);
const term = query.trim().toLowerCase();
const results = integrations.filter(
(item) =>
item.name.toLowerCase().includes(term) ||
item.category.toLowerCase().includes(term),
);
const search = (value: string) => {
setQuery(value);
inputRef.current?.focus();
};
return (
<div className="grid w-full max-w-md gap-3">
<InputGroup>
<InputGroupAddon>
<SearchIcon aria-hidden="true" />
</InputGroupAddon>
<InputGroupInput
ref={inputRef}
aria-label="Search integrations"
placeholder="Search integrations"
value={query}
onChange={(event) => setQuery(event.target.value)}
onKeyDown={(event) => {
if (event.key === "Escape") setQuery("");
}}
/>
{query ? (
<InputGroupAddon align="inline-end">
<InputGroupButton
size="icon-xs"
aria-label="Clear search"
onClick={() => search("")}
>
<XIcon aria-hidden="true" />
</InputGroupButton>
</InputGroupAddon>
) : null}
</InputGroup>
<div className="min-h-64 rounded-xl border">
<p aria-live="polite" className="sr-only">
{results.length} {results.length === 1 ? "result" : "results"}
</p>
{results.length > 0 ? (
<ul className="divide-y">
{results.map((item) => (
<li
key={item.name}
className="flex items-center justify-between gap-2 px-4 py-2.5 text-sm"
>
<span className="font-medium">{item.name}</span>
<span className="text-muted-foreground">{item.category}</span>
</li>
))}
</ul>
) : (
<Empty className="min-h-64">
<EmptyHeader>
<EmptyMedia variant="icon">
<SearchXIcon aria-hidden="true" />
</EmptyMedia>
<EmptyTitle className="break-words">
No integrations match “{query.trim()}”
</EmptyTitle>
<EmptyDescription>
Check the spelling, or try one of the popular ones below.
</EmptyDescription>
</EmptyHeader>
<EmptyContent>
<div className="flex flex-wrap justify-center gap-1.5">
{suggestions.map((name) => (
<Button
key={name}
size="xs"
variant="secondary"
onClick={() => search(name)}
>
{name}
</Button>
))}
</div>
<p className="text-xs text-muted-foreground">
Press <Kbd>Esc</Kbd> to clear the search
</p>
</EmptyContent>
</Empty>
)}
</div>
</div>
);
}
npx shadcn@latest add @sevenui/component/empty-10pnpm dlx shadcn@latest add @sevenui/component/empty-10yarn dlx shadcn@latest add @sevenui/component/empty-10bunx --bun shadcn@latest add @sevenui/component/empty-10Notifications
- PR
Priya Raman approved your pull request Add usage-based pricing table
4m ago
- DK
Daniel Kim assigned you to Fix flaky checkout test
1h ago
"use client";
import * as React from "react";
import {
AtSignIcon,
CheckCheckIcon,
InboxIcon,
SettingsIcon,
} from "lucide-react";
import { Avatar, AvatarFallback } from "@/components/ui/avatar";
import { Button } from "@/components/ui/button";
import {
Empty,
EmptyDescription,
EmptyHeader,
EmptyMedia,
EmptyTitle,
} from "@/components/ui/empty";
import {
Tabs,
TabsContent,
TabsList,
TabsTrigger,
} from "@/components/ui/tabs";
const initialNotifications = [
{
id: "n-1",
initials: "PR",
name: "Priya Raman",
action: "approved your pull request",
target: "Add usage-based pricing table",
time: "4m ago",
},
{
id: "n-2",
initials: "DK",
name: "Daniel Kim",
action: "assigned you to",
target: "Fix flaky checkout test",
time: "1h ago",
},
];
export default function Empty11() {
const [notifications, setNotifications] = React.useState(initialNotifications);
return (
<div className="w-full max-w-sm overflow-hidden rounded-xl border bg-card text-card-foreground shadow-sm">
<Tabs defaultValue="all">
<div className="flex items-center justify-between gap-2 border-b px-4 pt-3">
<h2 className="pb-3 text-sm font-medium">Notifications</h2>
<Button
size="xs"
variant="ghost"
className="mb-3"
disabled={notifications.length === 0}
onClick={() => setNotifications([])}
>
<CheckCheckIcon data-icon="inline-start" aria-hidden="true" />
Mark all read
</Button>
</div>
<TabsList variant="line" className="w-full justify-start gap-2 border-b px-3">
<TabsTrigger value="all" className="flex-none">
All
{notifications.length > 0 && (
<span className="rounded-full bg-primary px-1.5 text-[0.7rem] leading-4 text-primary-foreground tabular-nums">
{notifications.length}
</span>
)}
</TabsTrigger>
<TabsTrigger value="mentions" className="flex-none">
Mentions
</TabsTrigger>
</TabsList>
<TabsContent value="all" className="min-h-56">
{notifications.length > 0 ? (
<ul className="divide-y">
{notifications.map((item) => (
<li key={item.id} className="flex gap-3 px-4 py-3">
<Avatar size="sm">
<AvatarFallback>{item.initials}</AvatarFallback>
</Avatar>
<div className="min-w-0 flex-1 text-sm">
<p className="text-muted-foreground">
<span className="font-medium text-foreground">
{item.name}
</span>{" "}
{item.action}{" "}
<span className="font-medium text-foreground">
{item.target}
</span>
</p>
<p className="mt-0.5 text-xs text-muted-foreground">
{item.time}
</p>
</div>
</li>
))}
</ul>
) : (
<Empty className="min-h-56 rounded-none">
<EmptyHeader>
<EmptyMedia variant="icon">
<InboxIcon aria-hidden="true" />
</EmptyMedia>
<EmptyTitle>You're all caught up</EmptyTitle>
<EmptyDescription>
New reviews, assignments, and replies will land here.
</EmptyDescription>
</EmptyHeader>
</Empty>
)}
</TabsContent>
<TabsContent value="mentions" className="min-h-56">
<Empty className="min-h-56 rounded-none">
<EmptyHeader>
<EmptyMedia variant="icon">
<AtSignIcon aria-hidden="true" />
</EmptyMedia>
<EmptyTitle>No mentions yet</EmptyTitle>
<EmptyDescription>
When a teammate tags you with @ in a comment or doc, it shows up
here.
</EmptyDescription>
</EmptyHeader>
</Empty>
</TabsContent>
</Tabs>
<div className="border-t bg-muted/40 px-2 py-1.5">
<Button size="sm" variant="ghost" className="text-muted-foreground">
<SettingsIcon data-icon="inline-start" aria-hidden="true" />
Notification settings
</Button>
</div>
</div>
);
}
npx shadcn@latest add @sevenui/component/empty-11pnpm dlx shadcn@latest add @sevenui/component/empty-11yarn dlx shadcn@latest add @sevenui/component/empty-11bunx --bun shadcn@latest add @sevenui/component/empty-11Orders
0 of 4- Status: Refunded
- Region: EU
No orders match these filters
There are refunds in the US and orders in the EU, but none that are both. Loosen a filter to see them.
"use client";
import * as React from "react";
import { FilterXIcon, XIcon } from "lucide-react";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import {
Empty,
EmptyContent,
EmptyDescription,
EmptyHeader,
EmptyMedia,
EmptyTitle,
} from "@/components/ui/empty";
type Order = {
id: string;
customer: string;
status: "Paid" | "Refunded" | "Pending";
region: "EU" | "US";
total: string;
};
type Filter = { id: string; label: string; test: (order: Order) => boolean };
const orders: Order[] = [
{ id: "4821", customer: "Hannah Weber", status: "Refunded", region: "US", total: "$129.00" },
{ id: "4817", customer: "Luca Moretti", status: "Paid", region: "EU", total: "€84.50" },
{ id: "4809", customer: "Sofia Lindqvist", status: "Refunded", region: "US", total: "$42.00" },
{ id: "4796", customer: "Noah Becker", status: "Pending", region: "EU", total: "€210.00" },
];
const initialFilters: Filter[] = [
{ id: "status", label: "Status: Refunded", test: (order) => order.status === "Refunded" },
{ id: "region", label: "Region: EU", test: (order) => order.region === "EU" },
];
export default function Empty12() {
const [filters, setFilters] = React.useState(initialFilters);
const results = orders.filter((order) =>
filters.every((filter) => filter.test(order)),
);
function removeFilter(id: string) {
setFilters((current) => current.filter((filter) => filter.id !== id));
}
return (
<section
aria-labelledby="empty-12-title"
className="w-full max-w-md rounded-xl border bg-card text-card-foreground shadow-sm"
>
<header className="flex flex-col gap-2 border-b px-4 py-3">
<div className="flex items-center justify-between gap-2">
<h2 id="empty-12-title" className="text-sm font-medium">
Orders
</h2>
<span className="text-xs text-muted-foreground tabular-nums">
{results.length} of {orders.length}
</span>
</div>
{filters.length > 0 ? (
<ul aria-label="Active filters" className="flex flex-wrap gap-1.5">
{filters.map((filter) => (
<li key={filter.id}>
<Badge variant="secondary" className="h-6 gap-1 pr-0.5">
{filter.label}
<button
type="button"
aria-label={`Remove filter ${filter.label}`}
onClick={() => removeFilter(filter.id)}
className="flex size-5 items-center justify-center rounded-full outline-none hover:bg-background focus-visible:ring-2 focus-visible:ring-ring/50"
>
<XIcon aria-hidden="true" className="size-3" />
</button>
</Badge>
</li>
))}
</ul>
) : (
<p className="text-xs text-muted-foreground">No filters applied</p>
)}
</header>
<div aria-live="polite" className="min-h-64">
{results.length > 0 ? (
<ul className="divide-y">
{results.map((order) => (
<li
key={order.id}
className="flex items-center gap-3 px-4 py-2.5 text-sm"
>
<span className="w-12 shrink-0 text-muted-foreground tabular-nums">
#{order.id}
</span>
<span className="min-w-0 flex-1 truncate font-medium">
{order.customer}
</span>
<span className="hidden text-xs text-muted-foreground sm:inline">
{order.status}
</span>
<span className="tabular-nums">{order.total}</span>
</li>
))}
</ul>
) : (
<Empty className="min-h-64">
<EmptyHeader>
<EmptyMedia variant="icon">
<FilterXIcon aria-hidden="true" />
</EmptyMedia>
<EmptyTitle>No orders match these filters</EmptyTitle>
<EmptyDescription>
There are refunds in the US and orders in the EU, but none that
are both. Loosen a filter to see them.
</EmptyDescription>
</EmptyHeader>
<EmptyContent>
<div className="flex flex-wrap justify-center gap-2">
{filters.map((filter) => (
<Button
key={filter.id}
size="sm"
variant="outline"
onClick={() => removeFilter(filter.id)}
>
Drop “{filter.label}”
</Button>
))}
</div>
<Button size="sm" variant="link" onClick={() => setFilters([])}>
Clear all filters
</Button>
</EmptyContent>
</Empty>
)}
</div>
{filters.length < initialFilters.length && (
<div className="border-t px-4 py-2">
<Button
size="xs"
variant="ghost"
onClick={() => setFilters(initialFilters)}
>
Restore filters
</Button>
</div>
)}
</section>
);
}
npx shadcn@latest add @sevenui/component/empty-12pnpm dlx shadcn@latest add @sevenui/component/empty-12yarn dlx shadcn@latest add @sevenui/component/empty-12bunx --bun shadcn@latest add @sevenui/component/empty-12Your cart
0 itemsYour cart is empty
Orders over $75 ship free. Start with something from the list below.
Customers also bought
Waxed canvas tote
$68.00
Merino wool beanie
$34.00
Field notebook, 3-pack
$18.00
Subtotal$0.00
"use client";
import * as React from "react";
import { PlusIcon, ShoppingBagIcon, Trash2Icon } from "lucide-react";
import { Button } from "@/components/ui/button";
import {
Empty,
EmptyContent,
EmptyDescription,
EmptyHeader,
EmptyMedia,
EmptyTitle,
} from "@/components/ui/empty";
import { Separator } from "@/components/ui/separator";
const products = [
{ id: "canvas-tote", name: "Waxed canvas tote", variant: "Olive", price: 68 },
{ id: "wool-beanie", name: "Merino wool beanie", variant: "Charcoal", price: 34 },
{ id: "field-notes", name: "Field notebook, 3-pack", variant: "Dot grid", price: 18 },
];
const currency = new Intl.NumberFormat("en-US", {
style: "currency",
currency: "USD",
});
export default function Empty13() {
const [cart, setCart] = React.useState<string[]>([]);
const items = products.filter((product) => cart.includes(product.id));
const suggestions = products.filter((product) => !cart.includes(product.id));
const subtotal = items.reduce((sum, item) => sum + item.price, 0);
return (
<section
aria-labelledby="empty-13-title"
className="flex w-full max-w-sm flex-col rounded-xl border bg-card text-card-foreground shadow-sm"
>
<header className="flex items-center justify-between px-4 py-3">
<h2 id="empty-13-title" className="text-sm font-medium">
Your cart
</h2>
<span className="text-xs text-muted-foreground tabular-nums">
{items.length} {items.length === 1 ? "item" : "items"}
</span>
</header>
<Separator />
{items.length === 0 ? (
<Empty className="py-8">
<EmptyHeader>
<EmptyMedia variant="icon">
<ShoppingBagIcon aria-hidden="true" />
</EmptyMedia>
<EmptyTitle>Your cart is empty</EmptyTitle>
<EmptyDescription>
Orders over $75 ship free. Start with something from the list
below.
</EmptyDescription>
</EmptyHeader>
<EmptyContent>
<Button size="sm" variant="outline">
Browse the shop
</Button>
</EmptyContent>
</Empty>
) : (
<ul className="flex flex-col gap-3 px-4 py-4">
{items.map((item) => (
<li key={item.id} className="flex items-center gap-3">
<img
src="/placeholder.svg"
alt=""
className="size-12 shrink-0 rounded-md bg-muted object-cover"
/>
<div className="min-w-0 flex-1">
<p className="truncate text-sm font-medium">{item.name}</p>
<p className="text-xs text-muted-foreground">{item.variant}</p>
</div>
<span className="text-sm tabular-nums">
{currency.format(item.price)}
</span>
<Button
size="icon-sm"
variant="ghost"
aria-label={`Remove ${item.name}`}
onClick={() =>
setCart((current) => current.filter((id) => id !== item.id))
}
>
<Trash2Icon aria-hidden="true" />
</Button>
</li>
))}
</ul>
)}
{suggestions.length > 0 && (
<div className="border-t bg-muted/40 px-4 py-3">
<h3 className="mb-2 text-xs font-medium text-muted-foreground">
Customers also bought
</h3>
<ul className="flex flex-col gap-2">
{suggestions.map((product) => (
<li key={product.id} className="flex items-center gap-3">
<img
src="/placeholder.svg"
alt=""
className="size-9 shrink-0 rounded-md bg-muted object-cover"
/>
<div className="min-w-0 flex-1">
<p className="truncate text-sm">{product.name}</p>
<p className="text-xs text-muted-foreground tabular-nums">
{currency.format(product.price)}
</p>
</div>
<Button
size="xs"
variant="outline"
aria-label={`Add ${product.name} to cart`}
onClick={() => setCart((current) => [...current, product.id])}
>
<PlusIcon data-icon="inline-start" aria-hidden="true" />
Add
</Button>
</li>
))}
</ul>
</div>
)}
<div className="flex flex-col gap-3 border-t px-4 py-3">
<div className="flex items-center justify-between text-sm">
<span className="text-muted-foreground">Subtotal</span>
<span className="font-medium tabular-nums">
{currency.format(subtotal)}
</span>
</div>
<Button className="w-full" disabled={items.length === 0}>
Checkout
</Button>
</div>
</section>
);
}
npx shadcn@latest add @sevenui/component/empty-13pnpm dlx shadcn@latest add @sevenui/component/empty-13yarn dlx shadcn@latest add @sevenui/component/empty-13bunx --bun shadcn@latest add @sevenui/component/empty-13Team members
Invite people to the Acme workspace.
It's just you for now
Invite your team to review designs, comment, and ship together. You have 4 free seats left on the Team plan.
"use client";
import * as React from "react";
import { MailIcon, UserPlusIcon, XIcon } from "lucide-react";
import { Avatar, AvatarFallback, AvatarGroup } from "@/components/ui/avatar";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import {
Empty,
EmptyDescription,
EmptyHeader,
EmptyMedia,
EmptyTitle,
} from "@/components/ui/empty";
import { Input } from "@/components/ui/input";
import {
NativeSelect,
NativeSelectOption,
} from "@/components/ui/native-select";
const seatLimit = 5;
type Invite = { email: string; role: string };
export default function Empty14() {
const [invites, setInvites] = React.useState<Invite[]>([]);
const [email, setEmail] = React.useState("");
const [role, setRole] = React.useState("Editor");
const [error, setError] = React.useState("");
const seatsUsed = 1 + invites.length;
const full = seatsUsed >= seatLimit;
function handleSubmit(event: React.FormEvent<HTMLFormElement>) {
event.preventDefault();
const value = email.trim().toLowerCase();
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) {
setError("Enter a valid work email, like sam@acme.com.");
return;
}
if (invites.some((invite) => invite.email === value)) {
setError(`${value} already has a pending invite.`);
return;
}
setInvites((current) => [...current, { email: value, role }]);
setEmail("");
setError("");
}
return (
<section
aria-labelledby="empty-14-title"
className="w-full max-w-md rounded-xl border bg-card text-card-foreground shadow-sm"
>
<header className="flex items-start justify-between gap-4 p-4">
<div>
<h2 id="empty-14-title" className="text-sm font-medium">
Team members
</h2>
<p className="text-sm text-muted-foreground">
Invite people to the Acme workspace.
</p>
</div>
<span className="shrink-0 text-xs text-muted-foreground tabular-nums">
{seatsUsed} of {seatLimit} seats
</span>
</header>
<form
noValidate
onSubmit={handleSubmit}
className="flex flex-col gap-1.5 px-4"
>
<label htmlFor="empty-14-email" className="sr-only">
Email address
</label>
<div className="flex flex-col gap-2 sm:flex-row">
<Input
id="empty-14-email"
type="email"
placeholder="name@company.com"
value={email}
disabled={full}
aria-invalid={error ? true : undefined}
aria-describedby={error ? "empty-14-error" : undefined}
onChange={(event) => setEmail(event.target.value)}
className="flex-1"
/>
<div className="flex gap-2">
<NativeSelect
aria-label="Role"
value={role}
disabled={full}
onChange={(event) => setRole(event.target.value)}
className="flex-1 sm:flex-none"
>
<NativeSelectOption value="Admin">Admin</NativeSelectOption>
<NativeSelectOption value="Editor">Editor</NativeSelectOption>
<NativeSelectOption value="Viewer">Viewer</NativeSelectOption>
</NativeSelect>
<Button type="submit" disabled={full}>
Invite
</Button>
</div>
</div>
{error && (
<p id="empty-14-error" role="alert" className="text-xs text-destructive">
{error}
</p>
)}
</form>
<div className="p-4">
{invites.length === 0 ? (
<Empty className="border bg-muted/30 py-8">
<EmptyHeader>
<EmptyMedia aria-hidden="true">
<AvatarGroup>
<Avatar>
<AvatarFallback>ML</AvatarFallback>
</Avatar>
<div
aria-hidden="true"
className="flex size-8 items-center justify-center rounded-full border border-dashed border-muted-foreground/40 bg-card text-muted-foreground ring-2 ring-background"
>
<UserPlusIcon className="size-3.5" />
</div>
</AvatarGroup>
</EmptyMedia>
<EmptyTitle>It's just you for now</EmptyTitle>
<EmptyDescription>
Invite your team to review designs, comment, and ship together.
You have {seatLimit - 1} free seats left on the Team plan.
</EmptyDescription>
</EmptyHeader>
</Empty>
) : (
<ul aria-label="Pending invites" className="flex flex-col divide-y rounded-lg border">
{invites.map((invite) => (
<li key={invite.email} className="flex items-center gap-3 px-3 py-2.5">
<span className="flex size-8 shrink-0 items-center justify-center rounded-full bg-muted text-muted-foreground">
<MailIcon className="size-4" aria-hidden="true" />
</span>
<div className="min-w-0 flex-1">
<p className="truncate text-sm">{invite.email}</p>
<p className="text-xs text-muted-foreground">{invite.role}</p>
</div>
<Badge variant="secondary">Pending</Badge>
<Button
size="icon-sm"
variant="ghost"
aria-label={`Revoke invite for ${invite.email}`}
onClick={() =>
setInvites((current) =>
current.filter((item) => item.email !== invite.email),
)
}
>
<XIcon aria-hidden="true" />
</Button>
</li>
))}
</ul>
)}
</div>
</section>
);
}
npx shadcn@latest add @sevenui/component/empty-14pnpm dlx shadcn@latest add @sevenui/component/empty-14yarn dlx shadcn@latest add @sevenui/component/empty-14bunx --bun shadcn@latest add @sevenui/component/empty-14Top referrers
acme.dev · last 24 hours
No visits in the last 24 hours
The tracking script on acme.dev last reported 2 days ago. It may have been removed in your latest deploy.
"use client";
import * as React from "react";
import { ActivityIcon } from "lucide-react";
import { Button } from "@/components/ui/button";
import {
Empty,
EmptyContent,
EmptyDescription,
EmptyHeader,
EmptyMedia,
EmptyTitle,
} from "@/components/ui/empty";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group";
type Range = "24h" | "7d" | "30d";
const ranges: { value: Range; label: string; long: string }[] = [
{ value: "24h", label: "24h", long: "last 24 hours" },
{ value: "7d", label: "7d", long: "last 7 days" },
{ value: "30d", label: "30d", long: "last 30 days" },
];
const referrers: Record<Range, { source: string; visits: number }[]> = {
"24h": [],
"7d": [
{ source: "google.com", visits: 1284 },
{ source: "news.ycombinator.com", visits: 612 },
{ source: "github.com", visits: 331 },
{ source: "Direct", visits: 208 },
],
"30d": [
{ source: "google.com", visits: 5120 },
{ source: "news.ycombinator.com", visits: 2044 },
{ source: "github.com", visits: 1398 },
{ source: "x.com", visits: 910 },
],
};
const number = new Intl.NumberFormat("en-US");
export default function Empty15() {
const [range, setRange] = React.useState<Range>("24h");
const rows = referrers[range];
const total = rows.reduce((sum, row) => sum + row.visits, 0);
const current = ranges.find((item) => item.value === range);
return (
<section
aria-labelledby="empty-15-title"
className="flex w-full max-w-md flex-col rounded-xl border bg-card text-card-foreground shadow-sm"
>
<header className="flex flex-wrap items-center justify-between gap-3 border-b px-4 py-3">
<div>
<h2 id="empty-15-title" className="text-sm font-medium">
Top referrers
</h2>
<p className="text-xs text-muted-foreground">
acme.dev · {current?.long}
</p>
</div>
<ToggleGroup
aria-label="Date range"
variant="outline"
size="sm"
spacing={0}
value={[range]}
onValueChange={(value) => {
const next = value[0] as Range | undefined;
if (next) setRange(next);
}}
>
{ranges.map((item) => (
<ToggleGroupItem
key={item.value}
value={item.value}
aria-label={item.long}
className="px-2.5 tabular-nums"
>
{item.label}
</ToggleGroupItem>
))}
</ToggleGroup>
</header>
<div className="min-h-64 px-2 py-2" aria-live="polite">
{rows.length === 0 ? (
<Empty className="h-full min-h-60">
<EmptyHeader>
<EmptyMedia variant="icon">
<ActivityIcon aria-hidden="true" />
</EmptyMedia>
<EmptyTitle>No visits in the last 24 hours</EmptyTitle>
<EmptyDescription>
The tracking script on acme.dev last reported 2 days ago. It may
have been removed in your latest deploy.
</EmptyDescription>
</EmptyHeader>
<EmptyContent>
<div className="flex flex-wrap justify-center gap-2">
<Button size="sm">Check installation</Button>
<Button
size="sm"
variant="outline"
onClick={() => setRange("7d")}
>
View last 7 days
</Button>
</div>
</EmptyContent>
</Empty>
) : (
<Table>
<TableHeader>
<TableRow>
<TableHead>Source</TableHead>
<TableHead className="text-right">Visits</TableHead>
<TableHead className="w-16 text-right">Share</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{rows.map((row) => {
const share = Math.round((row.visits / total) * 100);
return (
<TableRow key={row.source}>
<TableCell className="max-w-40 truncate font-medium">
{row.source}
</TableCell>
<TableCell className="text-right tabular-nums">
{number.format(row.visits)}
</TableCell>
<TableCell className="text-right text-muted-foreground tabular-nums">
{share}%
</TableCell>
</TableRow>
);
})}
</TableBody>
</Table>
)}
</div>
</section>
);
}
npx shadcn@latest add @sevenui/component/empty-15pnpm dlx shadcn@latest add @sevenui/component/empty-15yarn dlx shadcn@latest add @sevenui/component/empty-15bunx --bun shadcn@latest add @sevenui/component/empty-15This folder is empty
Drag logos, mockups, or PDFs here. Files up to 250 MB are shared with everyone on Brand refresh.
"use client";
import * as React from "react";
import { cn } from "cn";
import { FileIcon, FolderPlusIcon, UploadCloudIcon, UploadIcon } from "lucide-react";
import {
Breadcrumb,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbList,
BreadcrumbPage,
BreadcrumbSeparator,
} from "@/components/ui/breadcrumb";
import { Button } from "@/components/ui/button";
import {
Empty,
EmptyContent,
EmptyDescription,
EmptyHeader,
EmptyMedia,
EmptyTitle,
} from "@/components/ui/empty";
type StoredFile = { id: number; name: string; size: number };
function formatSize(bytes: number) {
if (bytes < 1024) return `${bytes} B`;
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
}
export default function Empty16() {
const [files, setFiles] = React.useState<StoredFile[]>([]);
const [dragging, setDragging] = React.useState(false);
const inputRef = React.useRef<HTMLInputElement>(null);
const nextId = React.useRef(0);
function addFiles(list: FileList | null) {
if (!list || list.length === 0) return;
const next = Array.from(list, (file) => ({
id: nextId.current++,
name: file.name,
size: file.size,
}));
setFiles((current) => [...current, ...next]);
}
return (
<section
aria-labelledby="empty-16-title"
className="w-full max-w-lg rounded-xl border bg-card text-card-foreground shadow-sm"
>
<header className="flex flex-wrap items-center justify-between gap-3 border-b px-4 py-3">
<div className="min-w-0">
<Breadcrumb>
<BreadcrumbList>
<BreadcrumbItem>
<BreadcrumbLink href="/files">All files</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator />
<BreadcrumbItem>
<BreadcrumbLink href="/files/brand-refresh">
Brand refresh
</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator />
<BreadcrumbItem>
<BreadcrumbPage id="empty-16-title">Final exports</BreadcrumbPage>
</BreadcrumbItem>
</BreadcrumbList>
</Breadcrumb>
</div>
<div className="flex gap-2">
<Button size="sm" variant="outline">
<FolderPlusIcon data-icon="inline-start" aria-hidden="true" />
New folder
</Button>
<Button size="sm" onClick={() => inputRef.current?.click()}>
<UploadIcon data-icon="inline-start" aria-hidden="true" />
Upload
</Button>
</div>
</header>
<input
ref={inputRef}
type="file"
multiple
className="sr-only"
tabIndex={-1}
aria-hidden="true"
onChange={(event) => {
addFiles(event.target.files);
event.target.value = "";
}}
/>
<section
aria-label="Drop files to upload to Final exports"
className="p-4"
onDragOver={(event) => {
event.preventDefault();
setDragging(true);
}}
onDragLeave={(event) => {
if (!event.currentTarget.contains(event.relatedTarget as Node)) {
setDragging(false);
}
}}
onDrop={(event) => {
event.preventDefault();
setDragging(false);
addFiles(event.dataTransfer.files);
}}
>
{files.length === 0 ? (
<Empty
className={cn(
"border-2 py-10 transition-colors",
dragging ? "border-primary bg-primary/5" : "border-border",
)}
>
<EmptyHeader>
<EmptyMedia
className={cn(
"size-12 rounded-full bg-muted text-muted-foreground transition-transform",
dragging && "-translate-y-1 text-primary",
)}
>
<UploadCloudIcon className="size-6" aria-hidden="true" />
</EmptyMedia>
<EmptyTitle>
{dragging ? "Drop to upload" : "This folder is empty"}
</EmptyTitle>
<EmptyDescription>
Drag logos, mockups, or PDFs here. Files up to 250 MB are
shared with everyone on Brand refresh.
</EmptyDescription>
</EmptyHeader>
<EmptyContent>
<Button
size="sm"
variant="outline"
onClick={() => inputRef.current?.click()}
>
Choose files
</Button>
</EmptyContent>
</Empty>
) : (
<ul
aria-label="Files in Final exports"
className={cn(
"flex flex-col divide-y rounded-lg border transition-colors",
dragging && "border-primary bg-primary/5",
)}
>
{files.map((file) => (
<li
key={file.id}
className="flex items-center gap-3 px-3 py-2.5 text-sm"
>
<FileIcon
className="size-4 shrink-0 text-muted-foreground"
aria-hidden="true"
/>
<span className="min-w-0 flex-1 truncate">{file.name}</span>
<span className="text-xs text-muted-foreground tabular-nums">
{formatSize(file.size)}
</span>
</li>
))}
</ul>
)}
</section>
</section>
);
}
npx shadcn@latest add @sevenui/component/empty-16pnpm dlx shadcn@latest add @sevenui/component/empty-16yarn dlx shadcn@latest add @sevenui/component/empty-16bunx --bun shadcn@latest add @sevenui/component/empty-16Friday, Sep 25
No events
Your day is wide open
No meetings are booked. Protect a block before someone else fills it.
"use client";
import * as React from "react";
import { CalendarCheckIcon, CoffeeIcon, PlusIcon, Trash2Icon } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Calendar } from "@/components/ui/calendar";
import {
Empty,
EmptyContent,
EmptyDescription,
EmptyHeader,
EmptyMedia,
EmptyTitle,
} from "@/components/ui/empty";
type Meeting = { id: string; day: number; start: string; end: string; title: string };
const initialMeetings: Meeting[] = [
{ id: "m-1", day: 22, start: "10:00", end: "10:30", title: "Sprint planning" },
{ id: "m-2", day: 23, start: "14:00", end: "15:00", title: "Design review: onboarding" },
{ id: "m-3", day: 24, start: "09:30", end: "09:45", title: "Standup" },
{ id: "m-4", day: 28, start: "11:00", end: "12:00", title: "Quarterly roadmap sync" },
{ id: "m-5", day: 28, start: "16:00", end: "16:30", title: "1:1 with Jordan" },
];
const month = new Date(2026, 8, 1);
const dayLabel = new Intl.DateTimeFormat("en-US", {
weekday: "long",
month: "short",
day: "numeric",
});
const shortLabel = new Intl.DateTimeFormat("en-US", {
weekday: "short",
month: "short",
day: "numeric",
});
export default function Empty17() {
const [selected, setSelected] = React.useState<Date>(new Date(2026, 8, 25));
const [meetings, setMeetings] = React.useState(initialMeetings);
const day = selected.getDate();
const agenda = meetings.filter((meeting) => meeting.day === day);
const nextBusyDay = meetings
.map((meeting) => meeting.day)
.filter((value) => value > day)
.sort((a, b) => a - b)[0];
const busyDays = Array.from(
new Set(meetings.map((meeting) => meeting.day)),
(value) => new Date(2026, 8, value),
);
function blockFocusTime() {
setMeetings((current) => [
...current,
{
id: `focus-${day}`,
day,
start: "09:00",
end: "11:00",
title: "Focus time",
},
]);
}
return (
<section
aria-labelledby="empty-17-title"
className="@container w-full max-w-2xl rounded-xl border bg-card text-card-foreground shadow-sm"
>
<div className="flex flex-col @xl:flex-row">
<div className="flex justify-center border-b p-2 @xl:border-r @xl:border-b-0">
<Calendar
mode="single"
required
selected={selected}
onSelect={setSelected}
month={month}
disableNavigation
disabled={(date) => date.getMonth() !== month.getMonth()}
modifiers={{ busy: busyDays }}
modifiersClassNames={{
busy: "relative after:pointer-events-none after:absolute after:bottom-0.5 after:left-1/2 after:z-20 after:size-1 after:-translate-x-1/2 after:rounded-full after:bg-chart-2",
}}
className="bg-transparent"
/>
</div>
<div className="flex min-w-0 flex-1 flex-col p-4">
<h2 id="empty-17-title" className="text-sm font-medium">
{dayLabel.format(selected)}
</h2>
<p className="text-xs text-muted-foreground">
{agenda.length === 0
? "No events"
: `${agenda.length} ${agenda.length === 1 ? "event" : "events"}`}
</p>
<div className="mt-3 flex flex-1 flex-col" aria-live="polite">
{agenda.length === 0 ? (
<Empty className="flex-1 border bg-muted/30 px-4 py-6">
<EmptyHeader>
<EmptyMedia variant="icon">
<CoffeeIcon aria-hidden="true" />
</EmptyMedia>
<EmptyTitle>Your day is wide open</EmptyTitle>
<EmptyDescription>
No meetings are booked. Protect a block before someone
else fills it.
</EmptyDescription>
</EmptyHeader>
<EmptyContent>
<Button size="sm" onClick={blockFocusTime}>
<PlusIcon data-icon="inline-start" aria-hidden="true" />
Block 9–11 AM for focus
</Button>
{nextBusyDay && (
<Button
size="sm"
variant="link"
onClick={() => setSelected(new Date(2026, 8, nextBusyDay))}
>
Next event: {shortLabel.format(new Date(2026, 8, nextBusyDay))}
</Button>
)}
</EmptyContent>
</Empty>
) : (
<ul className="flex flex-col gap-2">
{agenda
.slice()
.sort((a, b) => a.start.localeCompare(b.start))
.map((meeting) => (
<li
key={meeting.id}
className="flex items-center gap-3 rounded-lg border bg-background px-3 py-2"
>
<CalendarCheckIcon
className="size-4 shrink-0 text-muted-foreground"
aria-hidden="true"
/>
<div className="min-w-0 flex-1">
<p className="truncate text-sm font-medium">
{meeting.title}
</p>
<p className="text-xs text-muted-foreground tabular-nums">
{meeting.start} – {meeting.end}
</p>
</div>
{meeting.id.startsWith("focus-") && (
<Button
size="icon-sm"
variant="ghost"
aria-label="Remove focus time"
onClick={() =>
setMeetings((current) =>
current.filter((item) => item.id !== meeting.id),
)
}
>
<Trash2Icon aria-hidden="true" />
</Button>
)}
</li>
))}
</ul>
)}
</div>
</div>
</div>
</section>
);
}
npx shadcn@latest add @sevenui/component/empty-17pnpm dlx shadcn@latest add @sevenui/component/empty-17yarn dlx shadcn@latest add @sevenui/component/empty-17bunx --bun shadcn@latest add @sevenui/component/empty-17API keys
Authenticate requests to api.acme.dev
No API keys yet
Create a secret key to call the API from your server. Pass it as a bearer token:
$ curl https://api.acme.dev/v1/orders \
-H "Authorization: Bearer $ACME_KEY""use client";
import * as React from "react";
import { CheckIcon, CopyIcon, KeyRoundIcon, TriangleAlertIcon } from "lucide-react";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import {
Empty,
EmptyContent,
EmptyDescription,
EmptyHeader,
EmptyMedia,
EmptyTitle,
} from "@/components/ui/empty";
import { Input } from "@/components/ui/input";
import {
InputGroup,
InputGroupAddon,
InputGroupButton,
InputGroupInput,
} from "@/components/ui/input-group";
type ApiKey = { id: string; name: string; secret: string; created: string };
function generateSecret() {
const alphabet = "abcdefghijkmnopqrstuvwxyz23456789";
let body = "";
for (let index = 0; index < 32; index++) {
body += alphabet[Math.floor(Math.random() * alphabet.length)];
}
return `acme_sk_${body}`;
}
export default function Empty18() {
const [keys, setKeys] = React.useState<ApiKey[]>([]);
const [creating, setCreating] = React.useState(false);
const [name, setName] = React.useState("Production server");
const [revealed, setRevealed] = React.useState<ApiKey | null>(null);
const [copied, setCopied] = React.useState(false);
const nameRef = React.useRef<HTMLInputElement>(null);
React.useEffect(() => {
if (creating) nameRef.current?.focus();
}, [creating]);
React.useEffect(() => {
if (!copied) return;
const timeout = window.setTimeout(() => setCopied(false), 2000);
return () => window.clearTimeout(timeout);
}, [copied]);
function createKey(event: React.FormEvent<HTMLFormElement>) {
event.preventDefault();
const key: ApiKey = {
id: `key-${keys.length + 1}`,
name: name.trim() || "Untitled key",
secret: generateSecret(),
created: "Just now",
};
setKeys((current) => [...current, key]);
setRevealed(key);
setCreating(false);
}
async function copySecret(secret: string) {
try {
await navigator.clipboard?.writeText(secret);
setCopied(true);
} catch {
// Clipboard access can be blocked; the field stays selectable.
}
}
return (
<section
aria-labelledby="empty-18-title"
className="w-full max-w-lg rounded-xl border bg-card text-card-foreground shadow-sm"
>
<header className="flex items-center justify-between gap-3 border-b px-4 py-3">
<div>
<h2 id="empty-18-title" className="text-sm font-medium">
API keys
</h2>
<p className="text-xs text-muted-foreground">
Authenticate requests to api.acme.dev
</p>
</div>
{keys.length > 0 && !creating && (
<Button size="sm" variant="outline" onClick={() => setCreating(true)}>
Create key
</Button>
)}
</header>
<div className="flex flex-col gap-4 p-4">
{creating && (
<form
onSubmit={createKey}
className="flex flex-col gap-2 rounded-lg border bg-muted/30 p-3"
>
<label htmlFor="empty-18-name" className="text-sm font-medium">
Key name
</label>
<Input
ref={nameRef}
id="empty-18-name"
value={name}
onChange={(event) => setName(event.target.value)}
placeholder="e.g. Production server"
/>
<div className="flex justify-end gap-2">
<Button
type="button"
size="sm"
variant="ghost"
onClick={() => setCreating(false)}
>
Cancel
</Button>
<Button type="submit" size="sm">
Create secret key
</Button>
</div>
</form>
)}
{revealed && (
<div
role="status"
className="flex flex-col gap-2 rounded-lg border border-warning/40 bg-warning/10 p-3"
>
<p className="flex items-center gap-2 text-sm font-medium">
<TriangleAlertIcon className="size-4 text-warning-foreground dark:text-warning" aria-hidden="true" />
Copy your key now
</p>
<p className="text-xs text-muted-foreground">
For security, “{revealed.name}” is shown only once.
Store it in your secrets manager.
</p>
<InputGroup className="bg-background">
<InputGroupInput
readOnly
value={revealed.secret}
aria-label="Secret key"
className="font-mono text-xs"
onFocus={(event) => event.currentTarget.select()}
/>
<InputGroupAddon align="inline-end">
<InputGroupButton
size="icon-xs"
aria-label={copied ? "Copied" : "Copy secret key"}
onClick={() => copySecret(revealed.secret)}
>
{copied ? (
<CheckIcon aria-hidden="true" />
) : (
<CopyIcon aria-hidden="true" />
)}
</InputGroupButton>
</InputGroupAddon>
</InputGroup>
<Button
size="sm"
variant="outline"
className="self-end"
onClick={() => setRevealed(null)}
>
I've saved it
</Button>
</div>
)}
{keys.length === 0 && !creating ? (
<Empty className="border py-8">
<EmptyHeader>
<EmptyMedia variant="icon">
<KeyRoundIcon aria-hidden="true" />
</EmptyMedia>
<EmptyTitle>No API keys yet</EmptyTitle>
<EmptyDescription>
Create a secret key to call the API from your server. Pass it as
a bearer token:
</EmptyDescription>
</EmptyHeader>
<EmptyContent className="max-w-md">
<pre className="w-full overflow-x-auto rounded-lg bg-muted px-3 py-2.5 text-left font-mono text-xs leading-relaxed">
<code>
<span className="text-muted-foreground">$ </span>curl
https://api.acme.dev/v1/orders \{"\n"}
{" "}-H "Authorization: Bearer $ACME_KEY"
</code>
</pre>
<Button size="sm" onClick={() => setCreating(true)}>
Create secret key
</Button>
</EmptyContent>
</Empty>
) : (
keys.length > 0 && (
<ul aria-label="API keys" className="flex flex-col divide-y rounded-lg border">
{keys.map((key) => (
<li key={key.id} className="flex items-center gap-3 px-3 py-2.5">
<div className="min-w-0 flex-1">
<p className="truncate text-sm font-medium">{key.name}</p>
<p className="font-mono text-xs text-muted-foreground">
acme_sk_…{key.secret.slice(-4)}
</p>
</div>
<span className="text-xs text-muted-foreground">
{key.created}
</span>
<Badge variant="outline">Full access</Badge>
</li>
))}
</ul>
)
)}
</div>
</section>
);
}
npx shadcn@latest add @sevenui/component/empty-18pnpm dlx shadcn@latest add @sevenui/component/empty-18yarn dlx shadcn@latest add @sevenui/component/empty-18bunx --bun shadcn@latest add @sevenui/component/empty-18MOTBAN
Acme support
Online · replies in ~5 min
How can we help?
Pick a topic or write your own. Maya, Theo, and Aiko are on shift until 6 PM CET.
"use client";
import * as React from "react";
import {
BugIcon,
CreditCardIcon,
MessagesSquareIcon,
SendIcon,
SparklesIcon,
} from "lucide-react";
import { Avatar, AvatarFallback, AvatarGroup } from "@/components/ui/avatar";
import { Button } from "@/components/ui/button";
import {
Empty,
EmptyContent,
EmptyDescription,
EmptyHeader,
EmptyMedia,
EmptyTitle,
} from "@/components/ui/empty";
import { Textarea } from "@/components/ui/textarea";
const team = [
{ initials: "MO", name: "Maya Ortiz" },
{ initials: "TB", name: "Theo Brandt" },
{ initials: "AN", name: "Aiko Nakamura" },
];
const topics = [
{
icon: CreditCardIcon,
label: "Billing question",
draft: "Hi! I have a question about my latest invoice: ",
},
{
icon: BugIcon,
label: "Report a bug",
draft: "I found a bug. Steps to reproduce: ",
},
{
icon: SparklesIcon,
label: "Request a feature",
draft: "It would help my team if you could add ",
},
];
export default function Empty19() {
const [draft, setDraft] = React.useState("");
const [messages, setMessages] = React.useState<{ id: number; text: string }[]>([]);
const textareaRef = React.useRef<HTMLTextAreaElement>(null);
function startTopic(text: string) {
setDraft(text);
const field = textareaRef.current;
if (field) {
field.focus();
requestAnimationFrame(() =>
field.setSelectionRange(text.length, text.length),
);
}
}
function send(event?: React.FormEvent<HTMLFormElement>) {
event?.preventDefault();
const text = draft.trim();
if (!text) return;
setMessages((current) => [...current, { id: current.length + 1, text }]);
setDraft("");
}
return (
<section
aria-labelledby="empty-19-title"
className="flex h-[30rem] w-full max-w-sm flex-col overflow-hidden rounded-xl border bg-card text-card-foreground shadow-sm"
>
<header className="flex items-center gap-3 border-b bg-muted/40 px-4 py-3">
<AvatarGroup className="-space-x-1">
{team.map((member) => (
<Avatar key={member.initials}>
<AvatarFallback className="text-xs">
{member.initials}
</AvatarFallback>
</Avatar>
))}
</AvatarGroup>
<div className="min-w-0">
<h2 id="empty-19-title" className="text-sm font-medium">
Acme support
</h2>
<p className="flex items-center gap-1.5 text-xs text-muted-foreground">
<span className="size-1.5 rounded-full bg-success" aria-hidden="true" />
Online · replies in ~5 min
</p>
</div>
</header>
<div
role="log"
aria-label="Conversation"
className="flex min-h-0 flex-1 flex-col overflow-y-auto p-4"
>
{messages.length === 0 ? (
<Empty className="p-2">
<EmptyHeader>
<EmptyMedia variant="icon">
<MessagesSquareIcon aria-hidden="true" />
</EmptyMedia>
<EmptyTitle>How can we help?</EmptyTitle>
<EmptyDescription>
Pick a topic or write your own. Maya, Theo, and Aiko are on
shift until 6 PM CET.
</EmptyDescription>
</EmptyHeader>
<EmptyContent>
<div className="flex w-full flex-col gap-1.5">
{topics.map((topic) => (
<Button
key={topic.label}
variant="outline"
className="justify-start"
onClick={() => startTopic(topic.draft)}
>
<topic.icon
data-icon="inline-start"
className="text-muted-foreground"
aria-hidden="true"
/>
{topic.label}
</Button>
))}
</div>
</EmptyContent>
</Empty>
) : (
<ol className="mt-auto flex flex-col gap-2">
{messages.map((message) => (
<li
key={message.id}
className="max-w-[85%] self-end rounded-2xl rounded-br-md bg-primary px-3 py-2 text-sm text-primary-foreground"
>
{message.text}
</li>
))}
<li className="mt-2 self-center text-xs text-muted-foreground">
Maya usually replies within 5 minutes. We'll also email you.
</li>
</ol>
)}
</div>
<form onSubmit={send} className="flex items-end gap-2 border-t p-3">
<label htmlFor="empty-19-message" className="sr-only">
Message
</label>
<Textarea
ref={textareaRef}
id="empty-19-message"
rows={1}
value={draft}
placeholder="Write a message…"
className="max-h-28 min-h-9 resize-none"
onChange={(event) => setDraft(event.target.value)}
onKeyDown={(event) => {
if (event.key === "Enter" && !event.shiftKey) {
event.preventDefault();
send();
}
}}
/>
<Button
type="submit"
size="icon"
aria-label="Send message"
disabled={!draft.trim()}
className="size-9"
>
<SendIcon aria-hidden="true" />
</Button>
</form>
</section>
);
}
npx shadcn@latest add @sevenui/component/empty-19pnpm dlx shadcn@latest add @sevenui/component/empty-19yarn dlx shadcn@latest add @sevenui/component/empty-19bunx --bun shadcn@latest add @sevenui/component/empty-19September 2026
$0.00spent
Monthly budget $1,200.00
Nothing logged in September
Snap a receipt and we'll fill in the merchant, amount, and category for you.
"use client";
import * as React from "react";
import {
ChartPieIcon,
ChevronLeftIcon,
ChevronRightIcon,
HouseIcon,
PencilLineIcon,
ReceiptTextIcon,
ScanLineIcon,
SettingsIcon,
Trash2Icon,
WalletIcon,
} from "lucide-react";
import { Button } from "@/components/ui/button";
import {
Empty,
EmptyContent,
EmptyDescription,
EmptyHeader,
EmptyMedia,
EmptyTitle,
} from "@/components/ui/empty";
import { Progress, ProgressLabel, ProgressValue } from "@/components/ui/progress";
type Expense = { id: string; merchant: string; category: string; amount: number };
const budget = 1200;
const months = [
{
key: "2026-08",
label: "August 2026",
short: "August",
expenses: [
{ id: "e-1", merchant: "Trader Joe's", category: "Groceries", amount: 84.2 },
{ id: "e-2", merchant: "City Transit pass", category: "Transport", amount: 96 },
{ id: "e-3", merchant: "Ember Coffee", category: "Eating out", amount: 12.5 },
{ id: "e-4", merchant: "Northwind Electric", category: "Utilities", amount: 71.35 },
] as Expense[],
},
{ key: "2026-09", label: "September 2026", short: "September", expenses: [] as Expense[] },
];
const currency = new Intl.NumberFormat("en-US", {
style: "currency",
currency: "USD",
});
const tabs = [
{ label: "Home", icon: HouseIcon },
{ label: "Budgets", icon: WalletIcon },
{ label: "Insights", icon: ChartPieIcon },
{ label: "Settings", icon: SettingsIcon },
];
export default function Empty20() {
const [monthIndex, setMonthIndex] = React.useState(1);
const [added, setAdded] = React.useState<Expense[]>([]);
const month = months[monthIndex];
const expenses = monthIndex === 1 ? added : month.expenses;
const spent = expenses.reduce((sum, expense) => sum + expense.amount, 0);
function addSample() {
setAdded((current) => [
...current,
{
id: `new-${current.length + 1}`,
merchant: "Farmers market",
category: "Groceries",
amount: 23.4,
},
]);
}
return (
<div className="flex h-[36rem] w-full max-w-[22rem] flex-col overflow-hidden rounded-[2rem] border-[6px] border-muted bg-background text-foreground shadow-lg">
<header className="flex flex-col gap-4 px-5 pt-5 pb-4">
<div className="flex items-center justify-between">
<Button
size="icon-sm"
variant="ghost"
aria-label="Previous month"
disabled={monthIndex === 0}
onClick={() => setMonthIndex((index) => index - 1)}
>
<ChevronLeftIcon aria-hidden="true" />
</Button>
<h2 className="text-sm font-medium" aria-live="polite">
{month.label}
</h2>
<Button
size="icon-sm"
variant="ghost"
aria-label="Next month"
disabled={monthIndex === months.length - 1}
onClick={() => setMonthIndex((index) => index + 1)}
>
<ChevronRightIcon aria-hidden="true" />
</Button>
</div>
<div className="flex flex-col gap-3">
<p className="text-3xl font-semibold tracking-tight tabular-nums">
{currency.format(spent)}
<span className="ml-1.5 text-sm font-normal tracking-normal text-muted-foreground">
spent
</span>
</p>
<Progress value={Math.min(100, (spent / budget) * 100)} className="gap-1.5">
<ProgressLabel className="text-xs font-normal text-muted-foreground">
Monthly budget {currency.format(budget)}
</ProgressLabel>
<ProgressValue className="text-xs" />
</Progress>
</div>
</header>
<div className="flex min-h-0 flex-1 flex-col overflow-y-auto border-t bg-muted/30 px-4 py-4">
{expenses.length === 0 ? (
<Empty className="flex-1 p-2">
<EmptyHeader>
<EmptyMedia className="size-14 rounded-2xl bg-card text-muted-foreground shadow-sm ring-1 ring-border">
<ReceiptTextIcon className="size-6" aria-hidden="true" />
</EmptyMedia>
<EmptyTitle className="text-base">
Nothing logged in {month.short}
</EmptyTitle>
<EmptyDescription>
Snap a receipt and we'll fill in the merchant, amount, and
category for you.
</EmptyDescription>
</EmptyHeader>
<EmptyContent className="gap-2">
<Button size="lg" className="w-full">
<ScanLineIcon data-icon="inline-start" aria-hidden="true" />
Scan a receipt
</Button>
<Button size="lg" variant="outline" className="w-full" onClick={addSample}>
<PencilLineIcon data-icon="inline-start" aria-hidden="true" />
Add manually
</Button>
</EmptyContent>
</Empty>
) : (
<ul aria-label={`${month.short} expenses`} className="flex flex-col gap-2">
{expenses.map((expense) => (
<li
key={expense.id}
className="flex items-center gap-3 rounded-xl bg-card px-3 py-2.5 shadow-sm ring-1 ring-border"
>
<div className="min-w-0 flex-1">
<p className="truncate text-sm font-medium">{expense.merchant}</p>
<p className="text-xs text-muted-foreground">{expense.category}</p>
</div>
<span className="text-sm tabular-nums">
−{currency.format(expense.amount)}
</span>
{monthIndex === 1 && (
<Button
size="icon-sm"
variant="ghost"
aria-label={`Remove ${expense.merchant}`}
onClick={() =>
setAdded((current) =>
current.filter((item) => item.id !== expense.id),
)
}
>
<Trash2Icon aria-hidden="true" />
</Button>
)}
</li>
))}
</ul>
)}
</div>
<nav aria-label="Primary" className="grid grid-cols-4 border-t bg-background px-2 pt-1.5 pb-3">
{tabs.map((tab, index) => (
<button
key={tab.label}
type="button"
aria-current={index === 0 ? "page" : undefined}
className="flex flex-col items-center gap-1 rounded-lg py-1.5 text-[0.7rem] text-muted-foreground outline-none hover:text-foreground focus-visible:ring-3 focus-visible:ring-ring/50 aria-[current=page]:text-foreground"
>
<tab.icon className="size-5" aria-hidden="true" />
{tab.label}
</button>
))}
</nav>
</div>
);
}
npx shadcn@latest add @sevenui/component/empty-20pnpm dlx shadcn@latest add @sevenui/component/empty-20yarn dlx shadcn@latest add @sevenui/component/empty-20bunx --bun shadcn@latest add @sevenui/component/empty-20