Blocks
Application
App shells, dashboards, stats, tables, activity feeds.
App shell
Top navigation bar with brand, links and account actions above a sidebar + content two-pane layout.
import {
Avatar,
Card,
IconButton,
NavigationBar,
Sidebar,
SidebarBody,
SidebarHeader,
} from "@zephora/react";
const bellIcon = (
<svg viewBox="0 0 16 16" width="1em" height="1em" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
<path d="M8 2a4 4 0 0 0-4 4v2.5L2.5 11h11L12 8.5V6a4 4 0 0 0-4-4Z" />
<path d="M6.5 13a1.5 1.5 0 0 0 3 0" />
</svg>
);
const navSections = [
{ heading: "Overview", links: ["Dashboard", "Analytics", "Reports"] },
{ heading: "Manage", links: ["Team", "Billing", "Settings"] },
];
export function AppShell() {
return (
<div style={{ border: "1px solid var(--z-border)", borderRadius: 12, overflow: "hidden" }}>
<NavigationBar
bordered
brand={
<span style={{ display: "flex", alignItems: "center", gap: 8, fontWeight: 700 }}>
<span
style={{
width: 24,
height: 24,
borderRadius: 6,
background: "var(--z-primary)",
color: "var(--z-primary-fg)",
display: "grid",
placeItems: "center",
fontSize: 13,
}}
>
Z
</span>
Zephora
</span>
}
actions={
<span style={{ display: "flex", alignItems: "center", gap: 8 }}>
<IconButton variant="ghost" size="sm" aria-label="Notifications" icon={bellIcon} />
<Avatar name="Mia Chen" size="sm" />
</span>
}
>
<a href="#">Dashboard</a>
<a href="#">Projects</a>
<a href="#">Reports</a>
</NavigationBar>
<div style={{ display: "flex", minHeight: 300 }}>
<Sidebar width="12.5rem" style={{ flexShrink: 0 }}>
<SidebarHeader>
<strong style={{ fontSize: 14 }}>Acme Inc</strong>
</SidebarHeader>
<SidebarBody>
{navSections.map((section) => (
<div key={section.heading} style={{ marginBottom: 16 }}>
<div
style={{
fontSize: 11,
textTransform: "uppercase",
letterSpacing: "0.05em",
color: "var(--z-muted-fg)",
marginBottom: 6,
}}
>
{section.heading}
</div>
<nav style={{ display: "grid", gap: 2 }} aria-label={section.heading}>
{section.links.map((link) => (
<a
key={link}
href="#"
style={{
padding: "6px 8px",
borderRadius: 6,
fontSize: 14,
color: "inherit",
textDecoration: "none",
}}
>
{link}
</a>
))}
</nav>
</div>
))}
</SidebarBody>
</Sidebar>
<main style={{ flex: 1, minWidth: 0, padding: 16 }}>
<Card style={{ minHeight: 240, display: "grid", placeItems: "center" }}>
<span style={{ color: "var(--z-muted-fg)", fontSize: 14 }}>Page content</span>
</Card>
</main>
</div>
</div>
);
}Stats dashboard
Four KPI statistic cards with trends above a line chart and a donut chart on a responsive grid.
- Revenue
- Expenses
- Direct
- Search
- Social
- Referral
import { Badge, Card, CardBody, CardHeader, Chart, Statistic } from "@zephora/react";
const stats: Array<{ title: string; value: string; trend: "up" | "down"; delta: string }> = [
{ title: "Total revenue", value: "$48,210", trend: "up", delta: "+12.4%" },
{ title: "Active users", value: "8,912", trend: "up", delta: "+3.1%" },
{ title: "Conversion rate", value: "3.6%", trend: "down", delta: "-0.4%" },
{ title: "Avg. order value", value: "$86.40", trend: "up", delta: "+1.8%" },
];
export function StatsDashboard() {
return (
<div style={{ display: "grid", gap: 16 }}>
<div
style={{
display: "grid",
gap: 16,
gridTemplateColumns: "repeat(auto-fit, minmax(180px, 1fr))",
}}
>
{stats.map((stat) => (
<Card key={stat.title}>
<Statistic title={stat.title} value={stat.value} trend={stat.trend} />
<div style={{ display: "flex", alignItems: "center", gap: 8, marginTop: 4 }}>
<Badge variant="soft" size="sm" color={stat.trend === "up" ? "success" : "danger"}>
{stat.delta}
</Badge>
<span style={{ fontSize: 12, color: "var(--z-muted-fg)" }}>vs last month</span>
</div>
</Card>
))}
</div>
<div
style={{
display: "grid",
gap: 16,
gridTemplateColumns: "repeat(auto-fit, minmax(300px, 1fr))",
}}
>
<Card>
<CardHeader>Revenue vs expenses</CardHeader>
<CardBody>
<div style={{ overflowX: "auto" }}>
<Chart
type="line"
width={380}
height={200}
data={{
labels: ["Jan", "Feb", "Mar", "Apr", "May", "Jun"],
datasets: [
{ label: "Revenue", data: [12, 18, 15, 22, 26, 31] },
{ label: "Expenses", data: [8, 10, 9, 12, 13, 15] },
],
}}
/>
</div>
</CardBody>
</Card>
<Card>
<CardHeader>Traffic sources</CardHeader>
<CardBody>
<div style={{ overflowX: "auto" }}>
<Chart
type="donut"
width={380}
height={200}
data={{
labels: ["Direct", "Search", "Social", "Referral"],
datasets: [{ label: "Sessions", data: [42, 29, 18, 11] }],
}}
/>
</div>
</CardBody>
</Card>
</div>
</div>
);
}Data table with toolbar
User table with search, role filter and add action plus sorting, row selection and pagination.
| Status | |||||
|---|---|---|---|---|---|
| Ana Souza | ana@acme.com | Admin | Active | ||
| Liam Wong | liam@acme.com | Editor | Active | ||
| Sara Kim | sara@acme.com | Viewer | Invited | ||
| Tom Ito | tom@acme.com | Editor | Suspended |
import * as React from "react";
import {
Badge,
Button,
Card,
DataTable,
IconButton,
Input,
Select,
type DataTableColumn,
} from "@zephora/react";
const searchIcon = (
<svg viewBox="0 0 16 16" width="1em" height="1em" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" aria-hidden="true">
<circle cx="7" cy="7" r="4.5" />
<path d="M10.5 10.5 14 14" />
</svg>
);
const dotsIcon = (
<svg viewBox="0 0 16 16" width="1em" height="1em" fill="currentColor" aria-hidden="true">
<circle cx="3" cy="8" r="1.3" />
<circle cx="8" cy="8" r="1.3" />
<circle cx="13" cy="8" r="1.3" />
</svg>
);
const plusIcon = (
<svg viewBox="0 0 16 16" width="1em" height="1em" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" aria-hidden="true">
<path d="M8 3v10M3 8h10" />
</svg>
);
type UserStatus = "Active" | "Invited" | "Suspended";
interface User {
name: string;
email: string;
role: string;
status: UserStatus;
}
const users: User[] = [
{ name: "Ana Souza", email: "ana@acme.com", role: "Admin", status: "Active" },
{ name: "Liam Wong", email: "liam@acme.com", role: "Editor", status: "Active" },
{ name: "Sara Kim", email: "sara@acme.com", role: "Viewer", status: "Invited" },
{ name: "Tom Ito", email: "tom@acme.com", role: "Editor", status: "Suspended" },
{ name: "Mia Chen", email: "mia@acme.com", role: "Admin", status: "Active" },
{ name: "Noah Patel", email: "noah@acme.com", role: "Viewer", status: "Invited" },
];
const statusColors: Record<UserStatus, "success" | "info" | "danger"> = {
Active: "success",
Invited: "info",
Suspended: "danger",
};
const columns: Array<DataTableColumn<User>> = [
{ key: "name", header: "Name", sortable: true },
{ key: "email", header: "Email" },
{ key: "role", header: "Role", sortable: true },
{
key: "status",
header: "Status",
cell: (row) => (
<Badge variant="soft" size="sm" color={statusColors[row.status]}>
{row.status}
</Badge>
),
},
{
key: "actions",
header: "",
align: "end",
cell: (row) => (
<IconButton variant="ghost" size="sm" aria-label={"Actions for " + row.name} icon={dotsIcon} />
),
},
];
export function UsersTable() {
const [selected, setSelected] = React.useState<string[]>([]);
const [query, setQuery] = React.useState("");
const [role, setRole] = React.useState("");
const rows = role === "" ? users : users.filter((user) => user.role === role);
return (
<Card padding="none">
<div
style={{
display: "flex",
gap: 8,
flexWrap: "wrap",
alignItems: "center",
padding: 12,
borderBottom: "1px solid var(--z-border)",
}}
>
<div style={{ flex: "1 1 200px", minWidth: 160 }}>
<Input
size="sm"
fullWidth
placeholder="Search users…"
startAdornment={searchIcon}
value={query}
onChange={(event) => setQuery(event.target.value)}
/>
</div>
<Select
size="sm"
clearable
aria-label="Filter by role"
placeholder="All roles"
value={role}
onValueChange={setRole}
options={[
{ label: "Admin", value: "Admin" },
{ label: "Editor", value: "Editor" },
{ label: "Viewer", value: "Viewer" },
]}
/>
{selected.length > 0 && (
<span style={{ fontSize: 13, color: "var(--z-muted-fg)" }}>
{selected.length} selected
</span>
)}
<Button size="sm" startIcon={plusIcon} style={{ marginLeft: "auto" }}>
Add user
</Button>
</div>
<DataTable
size="sm"
columns={columns}
data={rows}
rowKey={(row) => row.email}
globalFilter={query}
pagination={{ pageSize: 4 }}
selection={{ selected, onSelectionChange: setSelected }}
/>
</Card>
);
}Activity feed
Card with a colored, icon-annotated timeline of recent events and a view-all footer action.
- 2 min agoDeployment completedv2.4.0 shipped to production
- 1 hour agoNew team memberAna Souza joined the Design team
- 3 hours agoUsage nearing limitAPI quota at 82% for this cycle
- 5 hours agoNew commentLiam replied on the Q3 roadmap doc
- YesterdayBuild failedPipeline #4821 failed on the lint step
import { Button, Card, CardBody, CardFooter, CardHeader, Timeline } from "@zephora/react";
const checkIcon = (
<svg viewBox="0 0 16 16" width="1em" height="1em" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
<path d="m3 8.5 3.2 3L13 4.5" />
</svg>
);
const userIcon = (
<svg viewBox="0 0 16 16" width="1em" height="1em" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" aria-hidden="true">
<circle cx="8" cy="5" r="2.5" />
<path d="M3 13.5c1-2.4 2.8-3.5 5-3.5s4 1.1 5 3.5" />
</svg>
);
const alertIcon = (
<svg viewBox="0 0 16 16" width="1em" height="1em" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
<path d="M8 1.8 14.6 13.4H1.4Z" />
<path d="M8 6.2v3.2" />
<path d="M8 11.6h.01" />
</svg>
);
const commentIcon = (
<svg viewBox="0 0 16 16" width="1em" height="1em" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
<path d="M2.5 3.5h11v7H8l-3 2.6v-2.6H2.5Z" />
</svg>
);
const xIcon = (
<svg viewBox="0 0 16 16" width="1em" height="1em" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" aria-hidden="true">
<path d="m4 4 8 8M12 4l-8 8" />
</svg>
);
export function ActivityFeed() {
return (
<Card style={{ maxWidth: 520, margin: "0 auto" }}>
<CardHeader>Recent activity</CardHeader>
<CardBody>
<Timeline
items={[
{
title: "Deployment completed",
description: "v2.4.0 shipped to production",
time: "2 min ago",
color: "success",
icon: checkIcon,
},
{
title: "New team member",
description: "Ana Souza joined the Design team",
time: "1 hour ago",
color: "primary",
icon: userIcon,
},
{
title: "Usage nearing limit",
description: "API quota at 82% for this cycle",
time: "3 hours ago",
color: "warning",
icon: alertIcon,
},
{
title: "New comment",
description: "Liam replied on the Q3 roadmap doc",
time: "5 hours ago",
color: "info",
icon: commentIcon,
},
{
title: "Build failed",
description: "Pipeline #4821 failed on the lint step",
time: "Yesterday",
color: "danger",
icon: xIcon,
},
]}
/>
</CardBody>
<CardFooter>
<Button variant="ghost" fullWidth>
View all activity
</Button>
</CardFooter>
</Card>
);
}Project board
Three kanban-style columns of task cards with tag chips, assignee avatar stacks and progress bars.
import { Avatar, AvatarGroup, Badge, Card, Chip, Progress } from "@zephora/react";
const board: Array<{
title: string;
tasks: Array<{
name: string;
tags: Array<{
label: string;
color: "primary" | "success" | "warning" | "danger" | "info" | "neutral";
}>;
people: string[];
progress: number;
}>;
}> = [
{
title: "To do",
tasks: [
{
name: "Design empty states",
tags: [{ label: "Design", color: "info" }],
people: ["Ana Souza", "Mia Chen"],
progress: 0,
},
{
name: "Billing webhooks",
tags: [{ label: "Backend", color: "warning" }, { label: "P1", color: "danger" }],
people: ["Liam Wong"],
progress: 0,
},
],
},
{
title: "In progress",
tasks: [
{
name: "Checkout redesign",
tags: [{ label: "Frontend", color: "primary" }],
people: ["Sara Kim", "Tom Ito", "Ana Souza", "Noah Patel"],
progress: 60,
},
{
name: "Search relevance tuning",
tags: [{ label: "Backend", color: "warning" }],
people: ["Noah Patel", "Liam Wong"],
progress: 35,
},
],
},
{
title: "Done",
tasks: [
{
name: "SSO integration",
tags: [{ label: "Auth", color: "success" }],
people: ["Mia Chen", "Tom Ito"],
progress: 100,
},
{
name: "Dark theme tokens",
tags: [{ label: "Design", color: "info" }],
people: ["Ana Souza"],
progress: 100,
},
],
},
];
export function ProjectBoard() {
return (
<div
style={{
display: "grid",
gap: 12,
gridTemplateColumns: "repeat(auto-fit, minmax(220px, 1fr))",
}}
>
{board.map((column) => (
<Card key={column.title} variant="filled" padding="sm">
<div style={{ display: "flex", alignItems: "center", justifyContent: "space-between" }}>
<strong style={{ fontSize: 14 }}>{column.title}</strong>
<Badge variant="soft" color="neutral" size="sm">
{column.tasks.length}
</Badge>
</div>
<div style={{ display: "grid", gap: 8 }}>
{column.tasks.map((task) => (
<Card key={task.name} variant="elevated" padding="sm">
<div style={{ display: "grid", gap: 8 }}>
<span style={{ fontSize: 14, fontWeight: 600 }}>{task.name}</span>
<div style={{ display: "flex", gap: 4, flexWrap: "wrap" }}>
{task.tags.map((tag) => (
<Chip key={tag.label} size="sm" color={tag.color}>
{tag.label}
</Chip>
))}
</div>
<div style={{ display: "flex", alignItems: "center", gap: 12 }}>
<AvatarGroup size="xs" max={3}>
{task.people.map((person) => (
<Avatar key={person} name={person} />
))}
</AvatarGroup>
<div style={{ flex: 1, minWidth: 0 }}>
<Progress
size="sm"
value={task.progress}
color={task.progress === 100 ? "success" : "primary"}
aria-label={"Progress for " + task.name}
/>
</div>
</div>
</div>
</Card>
))}
</div>
</Card>
))}
</div>
);
}Settings tabs
Tabbed settings page with a profile form, notification switches and a destructive delete-account confirmation.
Used for sign-in and notifications.
Minimum 8 characters.
import * as React from "react";
import {
Avatar,
Button,
Card,
ConfirmDialog,
FormField,
Input,
Password,
Switch,
Tab,
TabList,
TabPanel,
Tabs,
} from "@zephora/react";
const notificationPrefs = [
{ title: "Product updates", description: "News about features and improvements.", defaultChecked: true },
{ title: "Comment replies", description: "When someone replies to your comment.", defaultChecked: true },
{ title: "Weekly digest", description: "Summary of workspace activity every Monday.", defaultChecked: false },
{ title: "SMS alerts", description: "Critical alerts sent via text message.", defaultChecked: false },
];
export function SettingsTabs() {
const [confirmOpen, setConfirmOpen] = React.useState(false);
return (
<Card>
<Tabs defaultValue="profile">
<TabList aria-label="Settings sections">
<Tab value="profile">Profile</Tab>
<Tab value="notifications">Notifications</Tab>
<Tab value="security">Security</Tab>
</TabList>
<TabPanel value="profile">
<div style={{ display: "grid", gap: 16, paddingTop: 16, maxWidth: 480 }}>
<div style={{ display: "flex", alignItems: "center", gap: 12 }}>
<Avatar name="Mia Chen" size="lg" />
<Button variant="outline" size="sm">
Change photo
</Button>
</div>
<FormField label="Full name" name="fullName">
<Input fullWidth defaultValue="Mia Chen" />
</FormField>
<FormField label="Email" name="email" help="Used for sign-in and notifications.">
<Input fullWidth type="email" defaultValue="mia@acme.com" />
</FormField>
<div>
<Button>Save changes</Button>
</div>
</div>
</TabPanel>
<TabPanel value="notifications">
<div style={{ paddingTop: 8, maxWidth: 520 }}>
{notificationPrefs.map((pref) => (
<div
key={pref.title}
style={{
display: "flex",
alignItems: "center",
justifyContent: "space-between",
gap: 16,
padding: "12px 0",
borderBottom: "1px solid var(--z-border)",
}}
>
<div>
<div style={{ fontSize: 14, fontWeight: 600 }}>{pref.title}</div>
<div style={{ fontSize: 13, color: "var(--z-muted-fg)" }}>{pref.description}</div>
</div>
<Switch defaultChecked={pref.defaultChecked} aria-label={pref.title} />
</div>
))}
</div>
</TabPanel>
<TabPanel value="security">
<div style={{ display: "grid", gap: 16, paddingTop: 16, maxWidth: 480 }}>
<FormField label="Current password" name="currentPassword">
<Password fullWidth />
</FormField>
<FormField label="New password" name="newPassword" help="Minimum 8 characters.">
<Password fullWidth />
</FormField>
<div>
<Button>Update password</Button>
</div>
<div
style={{
display: "flex",
alignItems: "center",
justifyContent: "space-between",
gap: 16,
border: "1px solid var(--z-danger)",
borderRadius: 8,
padding: 12,
}}
>
<div>
<div style={{ fontSize: 14, fontWeight: 600 }}>Delete account</div>
<div style={{ fontSize: 13, color: "var(--z-muted-fg)" }}>
Permanently removes your account and data.
</div>
</div>
<Button variant="danger" size="sm" onClick={() => setConfirmOpen(true)}>
Delete…
</Button>
</div>
</div>
</TabPanel>
</Tabs>
<ConfirmDialog
open={confirmOpen}
onOpenChange={setConfirmOpen}
title="Delete account?"
description="This permanently deletes your account and all workspace data. This action cannot be undone."
confirmLabel="Delete account"
destructive
onConfirm={() => setConfirmOpen(false)}
/>
</Card>
);
}Command palette card
Quick-actions card with an inline command palette: filter input, grouped items and keyboard shortcuts.
import {
Card,
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
Kbd,
} from "@zephora/react";
const plusIcon = (
<svg viewBox="0 0 16 16" width="1em" height="1em" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" aria-hidden="true">
<path d="M8 3v10M3 8h10" />
</svg>
);
const searchIcon = (
<svg viewBox="0 0 16 16" width="1em" height="1em" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" aria-hidden="true">
<circle cx="7" cy="7" r="4.5" />
<path d="M10.5 10.5 14 14" />
</svg>
);
const moonIcon = (
<svg viewBox="0 0 16 16" width="1em" height="1em" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
<path d="M13.5 9.5A6 6 0 1 1 6.5 2.5a4.8 4.8 0 0 0 7 7Z" />
</svg>
);
const userIcon = (
<svg viewBox="0 0 16 16" width="1em" height="1em" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" aria-hidden="true">
<circle cx="8" cy="5" r="2.5" />
<path d="M3 13.5c1-2.4 2.8-3.5 5-3.5s4 1.1 5 3.5" />
</svg>
);
const cardIcon = (
<svg viewBox="0 0 16 16" width="1em" height="1em" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" aria-hidden="true">
<rect x="1.5" y="3.5" width="13" height="9" rx="1.5" />
<path d="M1.5 6.5h13" />
</svg>
);
export function QuickActions() {
return (
<Card padding="none" style={{ maxWidth: 560, margin: "0 auto" }}>
<div
style={{
display: "flex",
alignItems: "center",
justifyContent: "space-between",
padding: "12px 16px",
borderBottom: "1px solid var(--z-border)",
}}
>
<strong style={{ fontSize: 14 }}>Quick actions</strong>
<Kbd keys={["Ctrl", "K"]} size="sm" />
</div>
<div style={{ padding: 12 }}>
<Command>
<CommandInput placeholder="Type a command or search…" />
<CommandList style={{ maxHeight: 260, overflowY: "auto", marginTop: 8 }}>
<CommandEmpty>No results found.</CommandEmpty>
<CommandGroup heading="General">
<CommandItem value="New project" icon={plusIcon} shortcut={<Kbd size="sm">N</Kbd>}>
New project
</CommandItem>
<CommandItem value="Search docs" icon={searchIcon} shortcut={<Kbd size="sm">/</Kbd>}>
Search docs
</CommandItem>
<CommandItem value="Toggle theme" icon={moonIcon} keywords={["dark", "light"]}>
Toggle theme
</CommandItem>
</CommandGroup>
<CommandGroup heading="Team">
<CommandItem value="Invite member" icon={userIcon} shortcut={<Kbd size="sm">I</Kbd>}>
Invite member
</CommandItem>
<CommandItem value="Billing settings" icon={cardIcon} keywords={["payment", "plan"]}>
Billing settings
</CommandItem>
</CommandGroup>
</CommandList>
</Command>
</div>
</Card>
);
}Notification center
Unread notification list with count badge, mark-all-read action and an Empty state once cleared.
- ASAna Souza commented on Q3 roadmap2 min ago
- LWLiam Wong assigned you to Checkout redesign26 min ago
- SKSara Kim requested review on PR #4821 hour ago
- TITom Ito mentioned you in #design-crit3 hours ago
import * as React from "react";
import { Avatar, Badge, Button, Card, Empty } from "@zephora/react";
const demoNotifications = [
{ id: 1, name: "Ana Souza", action: "commented on", target: "Q3 roadmap", time: "2 min ago" },
{ id: 2, name: "Liam Wong", action: "assigned you to", target: "Checkout redesign", time: "26 min ago" },
{ id: 3, name: "Sara Kim", action: "requested review on", target: "PR #482", time: "1 hour ago" },
{ id: 4, name: "Tom Ito", action: "mentioned you in", target: "#design-crit", time: "3 hours ago" },
];
export function NotificationCenter() {
const [items, setItems] = React.useState(demoNotifications);
return (
<Card padding="none" style={{ maxWidth: 520, margin: "0 auto" }}>
<div
style={{
display: "flex",
alignItems: "center",
justifyContent: "space-between",
gap: 8,
padding: "12px 16px",
borderBottom: "1px solid var(--z-border)",
}}
>
<span style={{ display: "flex", alignItems: "center", gap: 8 }}>
<strong style={{ fontSize: 14 }}>Notifications</strong>
{items.length > 0 && (
<Badge color="primary" size="sm">
{items.length}
</Badge>
)}
</span>
<Button variant="ghost" size="sm" disabled={items.length === 0} onClick={() => setItems([])}>
Mark all read
</Button>
</div>
{items.length === 0 ? (
<div style={{ padding: 24 }}>
<Empty title="All caught up" description="New notifications will appear here.">
<Button variant="outline" size="sm" onClick={() => setItems(demoNotifications)}>
Restore demo data
</Button>
</Empty>
</div>
) : (
<ul style={{ listStyle: "none", margin: 0, padding: 0 }}>
{items.map((item) => (
<li
key={item.id}
style={{
display: "flex",
alignItems: "flex-start",
gap: 12,
padding: "12px 16px",
borderBottom: "1px solid var(--z-border)",
}}
>
<Badge dot color="primary">
<Avatar name={item.name} size="sm" />
</Badge>
<div style={{ flex: 1, minWidth: 0, fontSize: 14 }}>
<div>
<strong>{item.name}</strong> {item.action} <strong>{item.target}</strong>
</div>
<div style={{ fontSize: 12, color: "var(--z-muted-fg)", marginTop: 2 }}>
{item.time}
</div>
</div>
</li>
))}
</ul>
)}
</Card>
);
}Table with export dialog
Data table with a pre-export dialog: pick columns, set the file name, choose CSV / Excel (.xlsx) / PDF — powered by the DataTable ref handle and the dependency-free export helpers.
| Şükrü Öztürk | Engineering | 95000 | 68200.5 |
| Elif Kaya | Design | 78000 | 56900.25 |
| Mert Demir | Engineering | 88000 | 63400 |
| Zeynep Arslan | Product | 102000 | 72150.75 |
| Can Yılmaz | Design | 71000 | 52300 |
import * as React from "react";
import {
Button,
Checkbox,
DataTable,
Dialog,
DialogBody,
DialogHeader,
DialogTitle,
FormField,
Input,
Select,
dataTableToCsv,
dataTableToXlsx,
downloadCsv,
downloadXlsx,
exportDataTablePdf,
type DataTableColumn,
type DataTableHandle,
} from "@zephora/react";
interface Payment {
id: string;
employee: string;
department: string;
gross: number;
net: number;
}
const payments: Payment[] = [
{ id: "1", employee: "Şükrü Öztürk", department: "Engineering", gross: 95000, net: 68200.5 },
{ id: "2", employee: "Elif Kaya", department: "Design", gross: 78000, net: 56900.25 },
// …
];
const columns: Array<DataTableColumn<Payment>> = [
{ key: "employee", header: "Employee", sortable: true },
{ key: "department", header: "Department", sortable: true },
{ key: "gross", header: "Gross", sortable: true, align: "end" },
{ key: "net", header: "Net", sortable: true, align: "end" },
];
export function TableWithExportDialog() {
const tableRef = React.useRef<DataTableHandle<Payment>>(null);
const [open, setOpen] = React.useState(false);
const [filename, setFilename] = React.useState("payroll");
const [format, setFormat] = React.useState("xlsx");
const [picked, setPicked] = React.useState<Record<string, boolean>>(
Object.fromEntries(columns.map((c) => [c.key, true]))
);
const runExport = async () => {
const cols = columns.filter((c) => picked[c.key]);
const rows = tableRef.current?.getProcessedRows() ?? [];
if (format === "csv") downloadCsv(dataTableToCsv(cols, rows), `${filename}.csv`);
else if (format === "xlsx")
downloadXlsx(dataTableToXlsx(cols, rows, { sheetName: filename }), `${filename}.xlsx`);
else await exportDataTablePdf(cols, rows, { title: filename, orientation: "landscape" });
setOpen(false);
};
return (
<>
<Button onClick={() => setOpen(true)}>Export…</Button>
<Dialog open={open} onOpenChange={setOpen}>
<DialogHeader>
<DialogTitle>Export table</DialogTitle>
</DialogHeader>
<DialogBody>
<FormField label="File name">
<Input value={filename} onChange={(e) => setFilename(e.target.value)} />
</FormField>
<FormField label="Format">
<Select
value={format}
onValueChange={setFormat}
options={[
{ label: "CSV", value: "csv" },
{ label: "Excel (.xlsx)", value: "xlsx" },
{ label: "PDF", value: "pdf" },
]}
/>
</FormField>
{columns.map((c) => (
<Checkbox
key={c.key}
checked={picked[c.key]}
onCheckedChange={(v) => setPicked((p) => ({ ...p, [c.key]: v === true }))}
>
{String(c.header)}
</Checkbox>
))}
<Button onClick={() => void runExport()} disabled={!filename}>
Export
</Button>
</DialogBody>
</Dialog>
<DataTable<Payment>
ref={tableRef}
columns={columns}
data={payments}
rowKey={(r) => r.id}
pagination={{ pageSize: 5, pageSizeOptions: [5, 10], allowAll: true }}
stateKey="payroll-export-demo"
/>
</>
);
}