Skip to content

Page

Use PageHeader at the top of AppLayoutMain (see AppLayout) for the page’s own title and controls, and PageContent below it for the page body. The application-wide top bar is AppHeader.

Code
import { useRef, useState } from "react";
import {
AppLayout,
AppLayoutContent,
AppLayoutMain,
Badge,
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,
ListIcon,
MapIcon,
MoreHorizontalIcon,
PageContent,
PageHeader,
PageHeaderActions,
PageHeaderContent,
PageHeaderDescription,
PageHeaderEyebrow,
PageHeaderHeading,
PageHeaderMedia,
PageHeaderTitle,
PageHeaderToolbar,
RadioGroup,
RadioGroupItem,
SearchIcon,
TabsNav,
TabsNavLink,
} 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 (
<AppLayout>
<AppLayoutContent>
<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">
Account cards and tables go here.
</p>
</PageContent>
</AppLayoutMain>
</AppLayoutContent>
</AppLayout>
);
}
AppLayoutMain
├── PageHeader
│ ├── PageHeaderHeading
│ │ ├── PageHeaderMedia (optional)
│ │ └── PageHeaderContent
│ │ ├── PageHeaderEyebrow (optional)
│ │ ├── PageHeaderTitle
│ │ └── PageHeaderDescription (optional)
│ ├── PageHeaderToolbar (optional)
│ │ ├── TabsNav (optional)
│ │ ├── ButtonGroup (optional)
│ │ ├── Filter (optional)
│ │ └── PageHeaderActions
│ │ ├── Button (primary actions)
│ │ ├── DropdownMenu (two or more secondary actions)
│ │ └── InputGroup (search)
│ └── FilterChips (optional)
└── PageContent (optional)

Toolbar controls keep this order: TabsNav when the page has sub-pages, ButtonGroup for view switches, and the Filter popup. When the header has no toolbar row, place PageHeaderActions inside PageHeaderHeading instead — the actions then share the title’s line. The applied-filter row is FilterChips from Filter.

PageContent is optional: content that needs to run edge-to-edge skips it and sits directly in AppLayoutMain instead, as PageHeader’s sibling — see Pages.

Code examples live in Pages.

Page-level header at the top of the main content region, stacking up to three rows: heading, toolbar, and applied-filter chips. Renders a <header> element and accepts standard <header> props.

Right-aligned action cluster at the end of the toolbar row; in a header without a toolbar row, place it in the heading row instead. Holds only actions and search: primary actions as standalone buttons, secondary actions collapsed into a dropdown menu when there is more than one, and the search input last. Every other control belongs on the toolbar’s left side. Renders a <div> element and accepts standard <div> props.

Text column for the eyebrow, title, and description. Renders a <div> element and accepts standard <div> props.

Supporting line under the title. Renders a <p> element and accepts standard <p> props.

Context line above the title, e.g. the retailer a page is scoped to. Renders a <p> element and accepts standard <p> props.

Heading row grouping the media, text content, and — in a header without a toolbar row — the actions. Renders a <div> element and accepts standard <div> props.

Partner logo slot; sizes a nested img itself. Renders a <div> element and accepts standard <div> props.

Page title. Renders an <h1> element and accepts standard <h1> props.

Toolbar row for page-level controls such as tab navigation, view switches, and filter triggers — they fill the row’s left side, in that order, with actions and search kept to PageHeaderActions at the end. Renders a <div> element and accepts standard <div> props.

Page body below PageHeader, holding everything the page renders. Renders a <div> element and accepts standard <div> props.