Skip to content
UI

Page Header

The top line of a page, with its title, an optional hint, page actions and a row of tabs.

@zenoxai/ui/patterns/page-header Markdown

Alerts

<PageHeader
  className="w-full"
  title="Alerts"
  icon={<ShieldAlertIcon />}
  hint="Every alert raised by your detection rules in the last 90 days."
  actions={
    <>
      <Button variant="outline">
        <DownloadIcon data-icon="inline-start" />
        Export
      </Button>
      <Button>
        <PlusIcon data-icon="inline-start" />
        New rule
      </Button>
    </>
  }
/>

Usage

import { ShieldAlertIcon } from "lucide-react"

import { Button } from "@zenoxai/ui/components/button"
import { PageHeader } from "@zenoxai/ui/patterns/page-header"

export function AlertsHeader() {
  return (
    <PageHeader
      title="Alerts"
      icon={<ShieldAlertIcon />}
      hint="Every alert raised by your detection rules in the last 90 days."
      actions={<Button>New rule</Button>}
    />
  )
}

Every page starts with one PageHeader. It renders the page’s only <h1>, at the page step of the type scale (text-xl, semibold), so titles read the same on every screen. Do not build a page title out of a plain heading and a flex row.

  • icon is the module’s lucide icon, the same one the sidebar shows. Pass it bare; the header sizes it to 20px.
  • hint is one sentence that explains the page. It opens in a Help Tooltip next to the title and is never rendered as a paragraph, so the header stays one line tall. Leave it out when the title says enough.
  • actions holds the page’s filters and its primary action, right-aligned. Keep at most one default button here; the rest are outline.
  • tabs is a row under the title, usually a TabsList with variant="line".

Long titles truncate. Keep them to the name of the page; put the record name, counts and dates in the page body.

PageHeader renders only the header line. The band around it (padding, the bottom border, the background, a fixed height) belongs to the app shell, which knows whether the page scrolls under the header or with it. Do not add padding or a border to the header itself.

Examples

With tabs

When the page has views of the same records, pass a line TabsList to tabs and wrap the header and the panels in one Tabs. The line variant draws its own bottom rule, so the header needs no border of its own.

Alerts

42 open alerts across 6 tenants.
<Tabs defaultValue="open" className="w-full">
  <PageHeader
    title="Alerts"
    icon={<ShieldAlertIcon />}
    hint="Every alert raised by your detection rules in the last 90 days."
    actions={
      <Button>
        <PlusIcon data-icon="inline-start" />
        New rule
      </Button>
    }
    tabs={
      <TabsList variant="line">
        <TabsTrigger value="open">Open</TabsTrigger>
        <TabsTrigger value="assigned">Assigned to me</TabsTrigger>
        <TabsTrigger value="resolved">Resolved</TabsTrigger>
      </TabsList>
    }
  />
  <TabsContent value="open" className="pt-2 text-text-secondary">
    42 open alerts across 6 tenants.
  </TabsContent>
  <TabsContent value="assigned" className="pt-2 text-text-secondary">
    7 alerts assigned to you.
  </TabsContent>
  <TabsContent value="resolved" className="pt-2 text-text-secondary">
    318 alerts resolved in the last 30 days.
  </TabsContent>
</Tabs>

Title only

Settings and detail pages often need only the title and the icon.

Assets

function PageHeaderTitleOnly() {
  return <PageHeader className="w-full" title="Assets" icon={<ServerIcon />} />
}

Inside the app shell

The shell draws the band: here, horizontal padding, a bottom border and the surface background. The header inside it is the same component as in the other examples.

Vulnerabilities

Page content scrolls under the band.
<div className="w-full overflow-hidden rounded-md border border-border-subtle">
  {/* The band belongs to the app shell: padding, border and background. */}
  <div className="border-b border-border-subtle bg-surface px-6 py-4">
    <PageHeader
      title="Vulnerabilities"
      icon={<ShieldAlertIcon />}
      actions={<Button variant="outline">Export</Button>}
    />
  </div>
  <div className="bg-background px-6 py-8 text-text-secondary">
    Page content scrolls under the band.
  </div>
</div>

Accessibility

  • The title is an <h1>. Render one PageHeader per page, and start the sections below it at <h2>.
  • The icon is decorative: lucide icons render with aria-hidden.
  • The hint is a focusable button next to the <h1>, not inside it, so the heading’s accessible name stays the title alone. The button’s name is the hint text: keep it to one short sentence.
  • Name icon-only buttons in actions with aria-label.

API reference

Generated from the TypeScript source. className, style and children are accepted everywhere and omitted below.

PageHeader

PageHeader is the top of every page: one <h1> at the page step of the type scale (text-xl, semibold), an optional icon and hint, an action slot on the right, and an optional tab row underneath.

PropTypeDefault
actionsReactNode

Right-hand slot: filters, a primary action, or both.

-
hintstring

One-sentence description of the page, shown in an info tooltip next to the title. Never rendered as a paragraph: the header stays one line tall.

-
iconReactNode

Leading icon, usually the module's lucide icon at size-5.

-
tabsReactNode

Row under the title, typically a line-variant <TabsList>.

-
title*ReactNode

The page title, rendered as the page's single <h1>.

-
titleClassNamestring

Extra classes for the <h1>, merged last so they win.

-