App Header
Breadcrumbs, search and account actions in an application header.
Give your AI assistant this page as context.
Connect your library with MCPINTERACTIVE PREVIEW · SAMPLE DATA
blocks/app-headerUsage
Get this block from a saved library export or the connected CLI. The imports below refer to local files installed in your app.
Import styles/components.css, then styles/blocks.css, then styles/theme.css once. Wrap your app in ThemeScope from components/ui/utils so typography, focus and overlays share the theme. The installation guide covers runtime dependencies and React/Next.js setup.
This import is the entry point, not a complete integration example. Supply the required props and connect callbacks to your application. The full ZIP includes examples/blocks-preview.tsx with the populated preview and local sample callbacks; adapt those examples to your product.
import { AppHeaderBlock } from './components/blocks/application';
// See Source and props below for the complete typed interface.Design system
This block consumes your semantic colors, spacing, typography and shape tokens. Import the compiled styles and your theme once as described in Export.
Icons use Lucide React. Install lucide-react@1.47.0 alongside React and Base UI.
Dependencies: Sidebar, Avatar, Badge, Breadcrumb, Button, Card, Dialog, Dropdown, Field, Input, Progress, Switch, Tabs, Textarea, Alert.
Behavior and accessibility
- Provide meaningful labels for interactive controls.
- Connect action callbacks to your application logic.
- Verify keyboard navigation and contrast in your application.
Status: experimental during early access. Sample previews do not send emails, process payments or upload files to a service.
Source and props
This is the exported source, using the same implementation as the editor. The complete source group is included below; related blocks share it to avoid duplicating behavior.
"use client";
import { useState, type ComponentProps, type ReactNode } from "react";
import {
ArrowUpRight,
Bell,
Building2,
CheckCheck,
ChevronRight,
CreditCard,
FileText,
KeyRound,
Layers2,
LayoutGrid,
Plus,
Search,
ShieldCheck,
Trash2,
UserRound,
UserRoundPlus,
UsersRound,
} from "lucide-react";
import {
Sidebar,
Avatar,
Badge,
Breadcrumb,
Button,
Card,
Dialog,
Dropdown,
Field,
Input,
Progress,
Switch,
Tabs,
Textarea,
} from '../ui';
import { ActionForm, type FormAction } from "./common";
function WorkspaceHeading({
eyebrow,
title,
description,
action,
}: {
eyebrow: string;
title: string;
description?: string;
action?: ReactNode;
}) {
return (
<header className="cr-workspace-heading">
<div>
<span className="cr-workspace-eyebrow">{eyebrow}</span>
<h3>{title}</h3>
{description && <p>{description}</p>}
</div>
{action && <div className="cr-workspace-heading-action">{action}</div>}
</header>
);
}
export function AppSidebarBlock({
workspace,
items,
accountName,
}: {
workspace: string;
items: ComponentProps<typeof Sidebar>["items"];
accountName: string;
}) {
return (
<div className="cr-app-sidebar-block">
<Sidebar
brand={
<span className="cr-workspace-brand">
<span className="cr-workspace-brand-icon">
<Layers2 size={19} aria-hidden="true" />
</span>
<span>
<strong>{workspace}</strong>
<small>Workspace</small>
</span>
</span>
}
items={items.map((item) => ({
...item,
icon: item.icon ?? <LayoutGrid size={18} aria-hidden="true" />,
}))}
footer={
<div className="cr-workspace-account">
<Avatar name={accountName} size={34} />
<span>
<strong>{accountName}</strong>
<small>Account</small>
</span>
</div>
}
/>
</div>
);
}
export function AppHeaderBlock({
breadcrumbs,
accountName,
onSearch,
onSignOut,
}: {
breadcrumbs: ComponentProps<typeof Breadcrumb>["items"];
accountName: string;
onSearch: () => void;
onSignOut: () => void;
}) {
return (
<header className="cr-block-header cr-workspace-header">
<div className="cr-workspace-header-location">
<span className="cr-workspace-header-icon">
<Layers2 size={18} aria-hidden="true" />
</span>
<Breadcrumb items={breadcrumbs} />
</div>
<div className="cr-workspace-header-actions">
<Button variant="ghost" onClick={onSearch}>
<Search size={17} aria-hidden="true" />
<span>Search</span>
</Button>
<span className="cr-workspace-header-divider" aria-hidden="true" />
<Dropdown
trigger={
<>
<Avatar name={accountName} size={28} />
<span className="cr-workspace-header-name">{accountName}</span>
<span className="cr-sr-only">Account menu</span>
</>
}
items={[{ label: "Sign out", onSelect: onSignOut }]}
/>
</div>
</header>
);
}
export function ProfileBlock({
name,
email,
bio = "",
onSave,
}: {
name: string;
email: string;
bio?: string;
onSave: FormAction;
}) {
return (
<Card className="cr-workspace-block cr-workspace-profile">
<WorkspaceHeading
eyebrow="Account"
title="Your profile"
description="The details your team sees when you work together."
/>
<div className="cr-workspace-identity">
<Avatar name={name} size={58} />
<div>
<strong>{name}</strong>
<span>{email}</span>
</div>
<span className="cr-workspace-icon-tile">
<UserRound size={20} aria-hidden="true" />
</span>
</div>
<ActionForm onSubmit={onSave}>
<div className="cr-workspace-form-grid">
<Field label="Display name">
<Input
name="name"
defaultValue={name}
autoComplete="name"
required
/>
</Field>
<Field label="Email address">
<Input
name="email"
type="email"
defaultValue={email}
autoComplete="email"
required
/>
</Field>
</div>
<Field label="About you">
<Textarea
name="bio"
defaultValue={bio}
placeholder="A few words about you and your work."
/>
</Field>
</ActionForm>
</Card>
);
}
export function SettingsBlock({
workspaceName,
onSave,
onNotificationSave,
}: {
workspaceName: string;
onSave: FormAction;
onNotificationSave: FormAction;
}) {
return (
<Card className="cr-workspace-block cr-workspace-settings">
<WorkspaceHeading
eyebrow="Workspace"
title="Settings"
description="A few details that make this space yours."
action={
<span className="cr-workspace-icon-tile">
<Building2 size={20} aria-hidden="true" />
</span>
}
/>
<Tabs
label="Settings sections"
items={[
{
value: "general",
label: "General",
content: (
<ActionForm onSubmit={onSave}>
<Field label="Workspace name">
<Input
name="workspace"
defaultValue={workspaceName}
required
/>
</Field>
<Field label="Description">
<Textarea
name="description"
placeholder="What are you building?"
/>
</Field>
</ActionForm>
),
},
{
value: "notifications",
label: "Notifications",
content: (
<ActionForm onSubmit={onNotificationSave}>
<div className="cr-workspace-preference">
<div>
<span className="cr-workspace-icon-tile">
<FileText size={18} aria-hidden="true" />
</span>
<p>A summary of what happened across your workspace.</p>
</div>
<Switch name="digest" label="Weekly digest" defaultChecked />
</div>
<div className="cr-workspace-preference">
<div>
<span className="cr-workspace-icon-tile">
<Bell size={18} aria-hidden="true" />
</span>
<p>Stay in the loop when someone needs your attention.</p>
</div>
<Switch
name="mentions"
label="Mention notifications"
defaultChecked
/>
</div>
</ActionForm>
),
},
]}
/>
</Card>
);
}
export interface TeamMember {
id: string;
name: string;
email: string;
role: string;
}
export function TeamBlock({
members,
onInvite,
onRemove,
}: {
members: TeamMember[];
onInvite: FormAction;
onRemove: (id: string) => void | Promise<void>;
}) {
const [removing, setRemoving] = useState<string | null>(null),
[error, setError] = useState(false);
return (
<Card className="cr-workspace-block cr-workspace-team">
<WorkspaceHeading
eyebrow="People & access"
title="Your team"
description="Good work starts with the right people."
action={
<Dialog
trigger={
<>
<UserRoundPlus size={16} aria-hidden="true" />
Invite member
</>
}
title="Invite someone to your team"
description="Enter the email address of the person you’d like to invite."
footer={null}
>
<ActionForm
onSubmit={onInvite}
submitLabel="Send invitation"
successMessage="Invitation sent."
>
<Field label="Email address">
<Input
name="email"
type="email"
autoComplete="email"
placeholder="colleague@company.com"
required
/>
</Field>
</ActionForm>
</Dialog>
}
/>
<div className="cr-workspace-list-heading">
<span>
<UsersRound size={15} aria-hidden="true" />
Team members
</span>
<Badge variant="outline">
{members.length} {members.length === 1 ? "member" : "members"}
</Badge>
</div>
<ul className="cr-workspace-list cr-workspace-member-list">
{members.map((member) => (
<li key={member.id}>
<Avatar name={member.name} size={36} />
<div className="cr-workspace-person">
<strong>{member.name}</strong>
<span>{member.email}</span>
</div>
<Badge variant="outline">{member.role}</Badge>
<Button
variant="ghost"
size="sm"
className="cr-workspace-icon-button"
disabled={Boolean(removing)}
loading={removing === member.id}
aria-label={`Remove ${member.name}`}
onClick={async () => {
setRemoving(member.id);
setError(false);
try {
await onRemove(member.id);
} catch {
setError(true);
} finally {
setRemoving(null);
}
}}
>
{removing !== member.id && (
<Trash2 size={16} aria-hidden="true" />
)}
</Button>
</li>
))}
</ul>
{!members.length && (
<p className="cr-block-empty">
No team members yet. Invite someone to get started.
</p>
)}
{error && (
<p role="alert" className="cr-description">
Unable to remove this member.
</p>
)}
</Card>
);
}
export function BillingBlock({
plan,
price,
renewal,
usage,
limit,
onManage,
invoices,
}: {
plan: string;
price: string;
renewal: string;
usage: number;
limit: number;
onManage: () => void;
invoices: Array<{ id: string; date: string; amount: string; href: string }>;
}) {
const safeLimit = Number.isFinite(limit) && limit > 0 ? limit : 1;
const safeUsage = Number.isFinite(usage)
? Math.max(0, Math.min(usage, safeLimit))
: 0;
return (
<Card className="cr-workspace-block cr-workspace-billing">
<WorkspaceHeading
eyebrow="Subscription"
title="Billing"
description="Your plan, usage and invoices in one place."
action={
<span className="cr-workspace-icon-tile">
<CreditCard size={20} aria-hidden="true" />
</span>
}
/>
<div className="cr-workspace-plan">
<div className="cr-workspace-plan-main">
<Badge variant="outline">{plan} plan</Badge>
<strong className="cr-workspace-plan-price">{price}</strong>
<span className="cr-description">{renewal}</span>
</div>
<Button variant="outline" onClick={onManage}>
Manage plan
<ArrowUpRight size={16} aria-hidden="true" />
</Button>
</div>
<div className="cr-workspace-usage">
<div className="cr-workspace-list-heading">
<span>Team seats</span>
<span>
{usage} <span className="cr-description">/ {limit} used</span>
</span>
</div>
<Progress label="Seats used" value={safeUsage} max={safeLimit} />
</div>
<div className="cr-workspace-list-heading">
<span>Invoice history</span>
<Badge variant="outline">{invoices.length}</Badge>
</div>
<ul className="cr-workspace-list cr-workspace-invoice-list">
{invoices.map((invoice) => (
<li key={invoice.id}>
<span className="cr-workspace-icon-tile">
<FileText size={17} aria-hidden="true" />
</span>
<span>{invoice.date}</span>
<strong>{invoice.amount}</strong>
<a
className="cr-link cr-workspace-invoice-link"
href={invoice.href}
aria-label={`View invoice dated ${invoice.date}`}
>
View
<ArrowUpRight size={15} aria-hidden="true" />
</a>
</li>
))}
</ul>
{!invoices.length && (
<p className="cr-block-empty">
No invoices yet. They will appear here after your first payment.
</p>
)}
</Card>
);
}
export function ApiKeysBlock({
keys,
onCreate,
onRevoke,
}: {
keys: Array<{ id: string; name: string; prefix: string; created: string }>;
onCreate: FormAction;
onRevoke: (id: string) => void | Promise<void>;
}) {
const [error, setError] = useState(false),
[busy, setBusy] = useState<string | null>(null);
return (
<Card className="cr-workspace-block cr-workspace-keys">
<WorkspaceHeading
eyebrow="Developer settings"
title="API keys"
description="Connect your tools to your workspace."
action={
<Dialog
trigger={
<>
<Plus size={16} aria-hidden="true" />
Create key
</>
}
title="Create an API key"
description="Give this key a name that helps you recognize its integration."
footer={null}
>
<ActionForm
onSubmit={onCreate}
submitLabel="Create key"
successMessage="Key created. Follow your application’s secure delivery flow."
>
<Field label="Key name">
<Input
name="name"
required
placeholder="Production integration"
/>
</Field>
</ActionForm>
</Dialog>
}
/>
<div className="cr-workspace-security-note">
<ShieldCheck size={17} aria-hidden="true" />
<span>
Use keys on your server. Keep them out of public repositories and
client-side code.
</span>
</div>
<div className="cr-workspace-list-heading">
<span>Secret keys</span>
<Badge variant="outline">{keys.length}</Badge>
</div>
<ul className="cr-workspace-list cr-workspace-key-list">
{keys.map((key) => (
<li key={key.id}>
<span className="cr-workspace-icon-tile">
<KeyRound size={18} aria-hidden="true" />
</span>
<div className="cr-workspace-person">
<strong>{key.name}</strong>
<span>
<code>{key.prefix}••••</code>
<span className="cr-workspace-key-date">
Created {key.created}
</span>
</span>
</div>
<Button
variant="ghost"
size="sm"
disabled={Boolean(busy)}
loading={busy === key.id}
aria-label={`Revoke ${key.name}`}
onClick={async () => {
setBusy(key.id);
setError(false);
try {
await onRevoke(key.id);
} catch {
setError(true);
} finally {
setBusy(null);
}
}}
>
Revoke
</Button>
</li>
))}
</ul>
{!keys.length && (
<p className="cr-block-empty">
No API keys yet. Create a key to connect an integration.
</p>
)}
{error && (
<p role="alert" className="cr-description">
Unable to revoke this key.
</p>
)}
</Card>
);
}
export function NotificationsBlock({
items,
onReadAll,
onOpen,
}: {
items: Array<{
id: string;
title: string;
description: string;
time: string;
unread: boolean;
}>;
onReadAll: () => void;
onOpen: (id: string) => void;
}) {
const unread = items.filter((item) => item.unread).length;
return (
<Card className="cr-workspace-block cr-workspace-notifications">
<WorkspaceHeading
eyebrow="Your inbox"
title="Notifications"
description="The latest from your team and workspace."
action={
<Button
variant="ghost"
size="sm"
onClick={onReadAll}
disabled={!unread}
>
<CheckCheck size={16} aria-hidden="true" />
Mark all read
</Button>
}
/>
<div className="cr-workspace-list-heading">
<span>Recent activity</span>
<Badge variant={unread ? "primary" : "outline"}>{unread} unread</Badge>
</div>
<ul className="cr-workspace-list cr-workspace-notification-list">
{items.map((item) => (
<li key={item.id}>
<button
type="button"
className="cr-notification-item cr-workspace-notification"
data-unread={item.unread || undefined}
onClick={() => onOpen(item.id)}
>
<span className="cr-workspace-icon-tile">
{item.unread ? (
<Bell size={18} aria-hidden="true" />
) : (
<CheckCheck size={18} aria-hidden="true" />
)}
</span>
<span className="cr-workspace-notification-copy">
<strong>
{item.title}
{item.unread && (
<>
<span aria-hidden="true" className="cr-unread-dot" />
<span className="cr-sr-only">Unread</span>
</>
)}
</strong>
<span className="cr-description">{item.description}</span>
<small>{item.time}</small>
</span>
<ChevronRight size={16} aria-hidden="true" />
</button>
</li>
))}
</ul>
{!items.length && (
<div className="cr-workspace-caught-up">
<span className="cr-workspace-icon-tile">
<CheckCheck size={22} aria-hidden="true" />
</span>
<strong>You’re all caught up.</strong>
<p>New updates will appear here.</p>
</div>
)}
</Card>
);
}