Skip to content

Pages

Every page sits in AppLayoutMain (see Layout): a PageHeader for the page’s own title and controls, above a PageContent for the body — both documented together as Page. PageContent’s content varies by page — the most common shape is a table, documented in Table pages.

PageHeader stacks up to three rows: a heading, a toolbar — with a TabsNav when the page has sub-pages — and, always last, applied filter chips from Filter. PageHeaderActions holds only actions and search: primary actions as standalone buttons, two or more secondary actions collapsed into a DropdownMenu behind an ellipsis trigger, and search last — on narrow viewports it wraps below the buttons and takes the full row. Every other control — tabs, button groups, filter triggers — sits before it, on the toolbar’s left side.

Code
import { useRef, useState } from "react";
import {
AppHeader,
AppHeaderLeading,
AppHeaderNotifications,
AppHeaderTrailing,
AppLayout,
AppLayoutContent,
AppLayoutMain,
Badge,
Breadcrumb,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbList,
BreadcrumbPage,
BreadcrumbSeparator,
Button,
ButtonGroup,
Checkbox,
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
FieldIcon,
Filter,
FilterApply,
FilterButton,
FilterChip,
FilterChipRemove,
FilterChips,
FilterClearAll,
FilterContent,
FilterFooter,
FilterHeader,
FilterNav,
FilterNavFooter,
FilterNavItem,
FilterNavSaved,
FilterOption,
FilterOptionList,
FilterPane,
FilterPaneBack,
FilterPaneClear,
FilterPaneFooter,
FilterPaneHeader,
FilterPaneTitle,
FilterSave,
FilterSaveDialog,
FilterSavedItem,
FilterSavedItemButton,
InputGroup,
InputGroupAddon,
InputGroupInput,
LayoutDashboardIcon,
ListIcon,
MapIcon,
MoreHorizontalIcon,
PageContent,
PageHeader,
PageHeaderActions,
PageHeaderContent,
PageHeaderDescription,
PageHeaderEyebrow,
PageHeaderHeading,
PageHeaderMedia,
PageHeaderTitle,
PageHeaderToolbar,
RadioGroup,
RadioGroupItem,
SearchIcon,
Sidebar,
SidebarBrand,
SidebarContent,
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
SidebarProvider,
SidebarTrigger,
TabsNav,
TabsNavLink,
UsersIcon,
} from "@falcon/ui-kit";
const statuses = ["Any status", "Active", "Inactive"];
const regions = ["Midwest", "Great Plains", "Delta", "Southeast"];
interface Selection {
status: string;
regions: string[];
}
interface SavedFilter {
id: number;
name: string;
selection: Selection;
}
function countFilters(selection: Selection) {
return (
(selection.status === "Any status" ? 0 : 1) +
(selection.regions.length > 0 ? 1 : 0)
);
}
export function Example() {
const [open, setOpen] = useState(false);
const [category, setCategory] = useState("Status");
const [draft, setDraft] = useState<Selection>({
status: "Active",
regions: ["Midwest"],
});
const [applied, setApplied] = useState(draft);
const [saved, setSaved] = useState<SavedFilter[]>([]);
const [saveOpen, setSaveOpen] = useState(false);
const nextId = useRef(1);
const draftCount = countFilters(draft);
function clearDraft() {
setDraft({ status: "Any status", regions: [] });
}
function clearStatus() {
setDraft({ ...draft, status: "Any status" });
}
function clearRegions() {
setDraft({ ...draft, regions: [] });
}
function apply() {
setApplied(draft);
setOpen(false);
}
function applyBoth(next: Selection) {
setApplied(next);
setDraft(next);
}
const footer = (
<FilterFooter>
<FilterClearAll onClick={clearDraft} />
<FilterApply count={draftCount} onClick={apply} />
</FilterFooter>
);
return (
<SidebarProvider defaultOpen>
<AppLayout>
<Sidebar>
<SidebarBrand logo={<FieldIcon />} name="AgVend" />
<SidebarContent>
<SidebarMenu>
<SidebarMenuItem>
<SidebarMenuButton icon={<LayoutDashboardIcon size={16} />}>
Dashboard
</SidebarMenuButton>
</SidebarMenuItem>
<SidebarMenuItem>
<SidebarMenuButton icon={<UsersIcon size={16} />}>
Accounts
</SidebarMenuButton>
</SidebarMenuItem>
</SidebarMenu>
</SidebarContent>
</Sidebar>
<AppLayoutContent>
<AppHeader>
<AppHeaderLeading>
<SidebarTrigger />
<Breadcrumb>
<BreadcrumbList>
<BreadcrumbItem>
<BreadcrumbLink href="#accounts">Accounts</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator />
<BreadcrumbItem>
<BreadcrumbPage>All Accounts</BreadcrumbPage>
</BreadcrumbItem>
</BreadcrumbList>
</Breadcrumb>
</AppHeaderLeading>
<AppHeaderTrailing>
<AppHeaderNotifications aria-label="Notifications" count={3} />
</AppHeaderTrailing>
</AppHeader>
<AppLayoutMain>
<PageHeader>
<PageHeaderHeading>
<PageHeaderMedia>
<FieldIcon />
</PageHeaderMedia>
<PageHeaderContent>
<PageHeaderEyebrow>Agtegra Cooperative</PageHeaderEyebrow>
<PageHeaderTitle>
Accounts <Badge variant="secondary">288</Badge>
</PageHeaderTitle>
<PageHeaderDescription>
Last updated today
</PageHeaderDescription>
</PageHeaderContent>
</PageHeaderHeading>
<PageHeaderToolbar>
<TabsNav aria-label="Accounts">
<TabsNavLink href="#active" aria-current="page">
Active
</TabsNavLink>
<TabsNavLink href="#leads">Leads</TabsNavLink>
<TabsNavLink href="#notes">Notes</TabsNavLink>
</TabsNav>
<ButtonGroup>
<Button variant="outline">
<ListIcon /> List
</Button>
<Button variant="outline">
<MapIcon /> Map
</Button>
</ButtonGroup>
<Filter
open={open}
onOpenChange={(nextOpen) => {
if (nextOpen) {
setDraft(applied);
}
setOpen(nextOpen);
}}
>
<FilterButton>Filter</FilterButton>
<FilterContent>
<FilterHeader />
<FilterNav>
<FilterNavSaved
active={category === "Saved"}
applied={saved.length > 0}
onClick={() => setCategory("Saved")}
/>
<FilterNavItem
active={category === "Status"}
applied={draft.status !== "Any status"}
values={
draft.status !== "Any status" ? [draft.status] : []
}
onClick={() => setCategory("Status")}
>
Status
</FilterNavItem>
<FilterNavItem
active={category === "Region"}
applied={draft.regions.length > 0}
values={draft.regions}
onClick={() => setCategory("Region")}
>
Region
</FilterNavItem>
</FilterNav>
{category === "Saved" && (
<FilterPane>
<FilterPaneHeader>
<FilterPaneBack />
<FilterPaneTitle>Saved</FilterPaneTitle>
</FilterPaneHeader>
{saved.length === 0 ? (
<p className="tw:flex-1 tw:px-4 tw:py-2 tw:text-[0.8rem] tw:text-muted-foreground">
No saved filters yet. Use Save next to your active
filter chips to add one.
</p>
) : (
<FilterOptionList>
{saved.map((filter) => (
<FilterSavedItem key={filter.id}>
<FilterSavedItemButton
onClick={() => {
applyBoth(filter.selection);
setOpen(false);
}}
>
{filter.name}
</FilterSavedItemButton>
</FilterSavedItem>
))}
</FilterOptionList>
)}
{footer}
<FilterPaneFooter onClear={clearDraft} />
</FilterPane>
)}
{category === "Status" && (
<FilterPane>
<FilterPaneHeader>
<FilterPaneBack />
<FilterPaneTitle>Status</FilterPaneTitle>
{draft.status !== "Any status" && (
<FilterPaneClear onClick={clearStatus} />
)}
</FilterPaneHeader>
<FilterOptionList>
<RadioGroup
value={draft.status}
onValueChange={(status) =>
setDraft({ ...draft, status })
}
className="tw:gap-0.5"
>
{statuses.map((status) => (
<FilterOption key={status}>
<RadioGroupItem value={status} />
{status}
</FilterOption>
))}
</RadioGroup>
</FilterOptionList>
{footer}
<FilterPaneFooter onClear={clearStatus} />
</FilterPane>
)}
{category === "Region" && (
<FilterPane>
<FilterPaneHeader>
<FilterPaneBack />
<FilterPaneTitle>Region</FilterPaneTitle>
{draft.regions.length > 0 && (
<FilterPaneClear onClick={clearRegions} />
)}
</FilterPaneHeader>
<FilterOptionList>
{regions.map((region) => (
<FilterOption key={region}>
<Checkbox
checked={draft.regions.includes(region)}
onCheckedChange={(checked) =>
setDraft({
...draft,
regions: checked
? [...draft.regions, region]
: draft.regions.filter(
(r) => r !== region,
),
})
}
/>
{region}
</FilterOption>
))}
</FilterOptionList>
{footer}
<FilterPaneFooter onClear={clearRegions} />
</FilterPane>
)}
<FilterNavFooter>
<FilterClearAll onClick={clearDraft}>
Clear filters
</FilterClearAll>
<FilterApply count={draftCount} onClick={apply} />
</FilterNavFooter>
</FilterContent>
</Filter>
<PageHeaderActions>
<Button>Create lead</Button>
<DropdownMenu>
<DropdownMenuTrigger
render={
<Button variant="outline" aria-label="More actions" />
}
>
<MoreHorizontalIcon />
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuItem>Import accounts</DropdownMenuItem>
<DropdownMenuItem>Export accounts</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
<InputGroup>
<InputGroupAddon>
<SearchIcon />
</InputGroupAddon>
<InputGroupInput placeholder="Search accounts" />
</InputGroup>
</PageHeaderActions>
</PageHeaderToolbar>
{countFilters(applied) > 0 && (
<>
<FilterChips>
{applied.status !== "Any status" && (
<FilterChip category="Status" values={[applied.status]}>
<FilterChipRemove
aria-label="Remove status filter"
onClick={() =>
applyBoth({ ...applied, status: "Any status" })
}
/>
</FilterChip>
)}
{applied.regions.length > 0 && (
<FilterChip category="Region" values={applied.regions}>
<FilterChipRemove
aria-label="Remove region filter"
onClick={() => applyBoth({ ...applied, regions: [] })}
/>
</FilterChip>
)}
<FilterSave onClick={() => setSaveOpen(true)} />
</FilterChips>
<FilterSaveDialog
open={saveOpen}
onOpenChange={setSaveOpen}
onSave={(name) => {
setSaved([
...saved,
{ id: nextId.current++, name, selection: applied },
]);
}}
/>
</>
)}
</PageHeader>
<PageContent>
<p className="tw:text-sm tw:text-muted-foreground">
Accounts across every retailer go here.
</p>
</PageContent>
</AppLayoutMain>
</AppLayoutContent>
</AppLayout>
</SidebarProvider>
);
}

PageContent sits on a tinted backdrop that offsets its content from the page’s edges. That offset is optional: content that needs to run edge-to-edge skips PageContent and sits directly in AppLayoutMain, as PageHeader’s sibling. A single content block takes tw:flex-1 so it still fills the remaining height; a table page is an exception — see Table pages — where the whole page scrolls as one unit instead.

Code
import {
AppHeader,
AppHeaderLeading,
AppHeaderNotifications,
AppHeaderTrailing,
AppLayout,
AppLayoutContent,
AppLayoutMain,
Breadcrumb,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbList,
BreadcrumbPage,
BreadcrumbSeparator,
FieldIcon,
LayoutDashboardIcon,
PageHeader,
PageHeaderTitle,
Sidebar,
SidebarBrand,
SidebarContent,
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
SidebarProvider,
SidebarTrigger,
UsersIcon,
} from "@falcon/ui-kit";
export function Example() {
return (
<SidebarProvider defaultOpen>
<AppLayout>
<Sidebar>
<SidebarBrand logo={<FieldIcon />} name="AgVend" />
<SidebarContent>
<SidebarMenu>
<SidebarMenuItem>
<SidebarMenuButton icon={<LayoutDashboardIcon size={16} />}>
Dashboard
</SidebarMenuButton>
</SidebarMenuItem>
<SidebarMenuItem>
<SidebarMenuButton icon={<UsersIcon size={16} />}>
Accounts
</SidebarMenuButton>
</SidebarMenuItem>
</SidebarMenu>
</SidebarContent>
</Sidebar>
<AppLayoutContent>
<AppHeader>
<AppHeaderLeading>
<SidebarTrigger />
<Breadcrumb>
<BreadcrumbList>
<BreadcrumbItem>
<BreadcrumbLink href="#">Home</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator />
<BreadcrumbItem>
<BreadcrumbPage>Field map</BreadcrumbPage>
</BreadcrumbItem>
</BreadcrumbList>
</Breadcrumb>
</AppHeaderLeading>
<AppHeaderTrailing>
<AppHeaderNotifications aria-label="Notifications" count={3} />
</AppHeaderTrailing>
</AppHeader>
<AppLayoutMain>
<PageHeader>
<PageHeaderTitle>Field map</PageHeaderTitle>
</PageHeader>
<div className="tw:flex tw:flex-1 tw:items-center tw:justify-center tw:bg-muted tw:text-sm tw:text-muted-foreground">
Edge-to-edge content goes here, flush with every side.
</div>
</AppLayoutMain>
</AppLayoutContent>
</AppLayout>
</SidebarProvider>
);
}

Every header row is optional, including PageHeaderToolbar itself — the row holding tabs, button groups, and filter triggers. Without any of those, that row disappears entirely; it’s not just empty. PageHeaderActions (buttons, a collapsed dropdown, search) isn’t part of the toolbar row itself, so it isn’t skipped along with it — it moves up into the heading row and shares the title’s line instead of sitting below it.

Code
import {
AppHeader,
AppHeaderLeading,
AppHeaderNotifications,
AppHeaderTrailing,
AppLayout,
AppLayoutContent,
AppLayoutMain,
Breadcrumb,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbList,
BreadcrumbPage,
BreadcrumbSeparator,
Button,
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
FieldIcon,
InputGroup,
InputGroupAddon,
InputGroupInput,
LayoutDashboardIcon,
MoreHorizontalIcon,
PageContent,
PageHeader,
PageHeaderActions,
PageHeaderContent,
PageHeaderDescription,
PageHeaderHeading,
PageHeaderTitle,
SearchIcon,
Sidebar,
SidebarBrand,
SidebarContent,
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
SidebarProvider,
SidebarTrigger,
UsersIcon,
} from "@falcon/ui-kit";
export function Example() {
return (
<SidebarProvider defaultOpen>
<AppLayout>
<Sidebar>
<SidebarBrand logo={<FieldIcon />} name="AgVend" />
<SidebarContent>
<SidebarMenu>
<SidebarMenuItem>
<SidebarMenuButton icon={<LayoutDashboardIcon size={16} />}>
Dashboard
</SidebarMenuButton>
</SidebarMenuItem>
<SidebarMenuItem>
<SidebarMenuButton icon={<UsersIcon size={16} />}>
Accounts
</SidebarMenuButton>
</SidebarMenuItem>
</SidebarMenu>
</SidebarContent>
</Sidebar>
<AppLayoutContent>
<AppHeader>
<AppHeaderLeading>
<SidebarTrigger />
<Breadcrumb>
<BreadcrumbList>
<BreadcrumbItem>
<BreadcrumbLink href="#orders">Orders</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator />
<BreadcrumbItem>
<BreadcrumbPage>All Orders</BreadcrumbPage>
</BreadcrumbItem>
</BreadcrumbList>
</Breadcrumb>
</AppHeaderLeading>
<AppHeaderTrailing>
<AppHeaderNotifications aria-label="Notifications" count={3} />
</AppHeaderTrailing>
</AppHeader>
<AppLayoutMain>
<PageHeader>
<PageHeaderHeading>
<PageHeaderContent>
<PageHeaderTitle>Orders</PageHeaderTitle>
<PageHeaderDescription>
Orders across all locations
</PageHeaderDescription>
</PageHeaderContent>
<PageHeaderActions>
<Button>Create order</Button>
<DropdownMenu>
<DropdownMenuTrigger
render={
<Button variant="outline" aria-label="More actions" />
}
>
<MoreHorizontalIcon />
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuItem>Import orders</DropdownMenuItem>
<DropdownMenuItem>Export orders</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
<InputGroup>
<InputGroupAddon>
<SearchIcon />
</InputGroupAddon>
<InputGroupInput placeholder="Search orders" />
</InputGroup>
</PageHeaderActions>
</PageHeaderHeading>
</PageHeader>
<PageContent>
<p className="tw:text-sm tw:text-muted-foreground">
Order lines, totals, and delivery details go here.
</p>
</PageContent>
</AppLayoutMain>
</AppLayoutContent>
</AppLayout>
</SidebarProvider>
);
}