Page Header
Title, description, actions and tabs for a pagePage Header is the top of a page: optional banner and avatar, a line of meta, the title, a description, actions and tabs. Each part is a thin wrapper over a Kookie UI primitive with sensible defaults, so any prop those primitives take still works.
Installation
Install the dependencies.
npm install @kushagradhawan/kookie-uiCopy this file into components/blocks/page-header. Keep the folder names, since blocks import each other by relative path.
'use client';
import * as React from 'react';
import { Avatar, Box, Flex, Heading, Separator, Text } from '@kushagradhawan/kookie-ui';
import type { AvatarProps, BoxProps, FlexProps, HeadingProps, TextProps } from '@kushagradhawan/kookie-ui';
type Breakpoint = 'initial' | 'xs' | 'sm' | 'md' | 'lg' | 'xl';
type Responsive<T> = T | Partial<Record<Breakpoint, T>>;
type Layout = 'inline' | 'stacked';
function layoutToDirection(layout: Responsive<Layout>): Responsive<'row' | 'column'> {
const toDirection = (value: Layout) => (value === 'stacked' ? 'column' : 'row');
if (typeof layout === 'string') return toDirection(layout);
const result: Partial<Record<Breakpoint, 'row' | 'column'>> = {};
for (const [breakpoint, value] of Object.entries(layout) as [Breakpoint, Layout | undefined][]) {
if (value) result[breakpoint] = toDirection(value);
}
return result;
}
const PageHeaderContext = React.createContext({ hasBanner: false });
/* -------------------------------------------------------------------------------------------------
* Root
* -----------------------------------------------------------------------------------------------*/
type PageHeaderRootProps = FlexProps & {
/** Draw a separator under the header. Leave off when using `PageHeader.Tabs`. */
separator?: boolean;
};
const PageHeaderRoot = React.forwardRef<HTMLDivElement, PageHeaderRootProps>(
({ direction = 'column', gap = '4', separator = false, children, ...props }, ref) => {
const hasBanner = React.Children.toArray(children).some(
(child) => React.isValidElement(child) && child.type === PageHeaderBanner,
);
const context = React.useMemo(() => ({ hasBanner }), [hasBanner]);
return (
<PageHeaderContext.Provider value={context}>
<Flex ref={ref} direction={direction} gap={gap} {...props}>
{children}
{separator && <Separator size="4" />}
</Flex>
</PageHeaderContext.Provider>
);
},
);
PageHeaderRoot.displayName = 'PageHeader.Root';
/* -------------------------------------------------------------------------------------------------
* Banner
* -----------------------------------------------------------------------------------------------*/
type PageHeaderBannerProps = Omit<BoxProps, 'height'> & {
src: string;
alt?: string;
/** @default "200px" */
height?: string;
};
const PageHeaderBanner = React.forwardRef<HTMLDivElement, PageHeaderBannerProps>(
({ src, alt = '', height = '200px', style, ...props }, ref) => (
<Box
ref={ref}
width="100%"
height={height}
role="img"
aria-label={alt}
style={{
backgroundImage: `url(${src})`,
backgroundSize: 'cover',
backgroundPosition: 'center',
borderRadius: 'var(--radius-3)',
...style,
}}
{...props}
/>
),
);
PageHeaderBanner.displayName = 'PageHeader.Banner';
/* -------------------------------------------------------------------------------------------------
* Main — row holding media, content and actions
* -----------------------------------------------------------------------------------------------*/
type PageHeaderMainProps = FlexProps & {
/** `inline` puts actions beside the title; `stacked` puts them below. Responsive. */
layout?: Responsive<Layout>;
};
const PageHeaderMain = React.forwardRef<HTMLDivElement, PageHeaderMainProps>(
({ layout = 'inline', gap = '4', align = 'center', justify = 'between', direction, ...props }, ref) => (
<Flex
ref={ref}
direction={direction ?? layoutToDirection(layout)}
gap={gap}
align={align}
justify={justify}
{...props}
/>
),
);
PageHeaderMain.displayName = 'PageHeader.Main';
/* -------------------------------------------------------------------------------------------------
* Media — logo or avatar
* -----------------------------------------------------------------------------------------------*/
type PageHeaderMediaProps = Omit<AvatarProps, 'radius'> & {
/** `logo` is square, `avatar` is round. @default "avatar" */
type?: 'logo' | 'avatar';
/** Pull an avatar up over the banner. Needs a `PageHeader.Banner`. */
overlap?: boolean;
};
const PageHeaderMedia = React.forwardRef<HTMLImageElement, PageHeaderMediaProps>(
({ type = 'avatar', overlap = false, size = '6', style, ...props }, ref) => {
const { hasBanner } = React.useContext(PageHeaderContext);
const shouldOverlap = overlap && hasBanner && type === 'avatar';
return (
<Avatar
ref={ref}
radius={type === 'logo' ? 'small' : 'full'}
size={size}
style={{
...(shouldOverlap && {
marginTop: 'calc(var(--space-9) * -1)',
border: '4px solid var(--color-background)',
}),
...style,
}}
{...props}
/>
);
},
);
PageHeaderMedia.displayName = 'PageHeader.Media';
/* -------------------------------------------------------------------------------------------------
* Text parts
* -----------------------------------------------------------------------------------------------*/
const PageHeaderContent = React.forwardRef<HTMLDivElement, FlexProps>(
({ direction = 'column', gap = '2', flexGrow = '1', ...props }, ref) => (
<Flex ref={ref} direction={direction} gap={gap} flexGrow={flexGrow} {...props} />
),
);
PageHeaderContent.displayName = 'PageHeader.Content';
const PageHeaderMeta = React.forwardRef<HTMLDivElement, FlexProps>(
({ gap = '2', align = 'center', wrap = 'wrap', ...props }, ref) => (
<Flex ref={ref} gap={gap} align={align} wrap={wrap} {...props} />
),
);
PageHeaderMeta.displayName = 'PageHeader.Meta';
const PageHeaderTitle = React.forwardRef<HTMLHeadingElement, HeadingProps>(
({ as = 'h1', size = '9', weight = 'medium', ...props }, ref) => (
<Heading ref={ref} as={as} size={size} weight={weight} {...props} />
),
);
PageHeaderTitle.displayName = 'PageHeader.Title';
const PageHeaderDescription = React.forwardRef<HTMLParagraphElement, TextProps>(
({ size = '4', emphasis = 'medium', ...props }, ref) => <Text ref={ref} size={size} emphasis={emphasis} {...props} />,
);
PageHeaderDescription.displayName = 'PageHeader.Description';
const PageHeaderActions = React.forwardRef<HTMLDivElement, FlexProps>(
({ gap = '2', align = 'center', ...props }, ref) => <Flex ref={ref} gap={gap} align={align} {...props} />,
);
PageHeaderActions.displayName = 'PageHeader.Actions';
const PageHeaderTabs = React.forwardRef<HTMLDivElement, FlexProps>(
({ direction = 'column', width = '100%', ...props }, ref) => (
<Flex ref={ref} direction={direction} width={width} {...props} />
),
);
PageHeaderTabs.displayName = 'PageHeader.Tabs';
export const PageHeader = Object.assign(PageHeaderRoot, {
Root: PageHeaderRoot,
Banner: PageHeaderBanner,
Main: PageHeaderMain,
Media: PageHeaderMedia,
Content: PageHeaderContent,
Meta: PageHeaderMeta,
Title: PageHeaderTitle,
Description: PageHeaderDescription,
Actions: PageHeaderActions,
Tabs: PageHeaderTabs,
});
Usage
import { PageHeader } from '@/components/blocks/page-header/page-header';
<PageHeader.Root separator>
<PageHeader.Main>
<PageHeader.Content>
<PageHeader.Title>Projects</PageHeader.Title>
<PageHeader.Description>Everything your team is working on.</PageHeader.Description>
</PageHeader.Content>
<PageHeader.Actions>
<Button variant="classic">New project</Button>
</PageHeader.Actions>
</PageHeader.Main>
</PageHeader.Root>Anatomy
<PageHeader.Root>
<PageHeader.Banner />
<PageHeader.Main>
<PageHeader.Media />
<PageHeader.Content>
<PageHeader.Meta />
<PageHeader.Title />
<PageHeader.Description />
</PageHeader.Content>
<PageHeader.Actions />
</PageHeader.Main>
<PageHeader.Tabs />
</PageHeader.Root>Props
Every part accepts the props of the component it wraps. Only the additions and changed defaults are listed.
| Part | Wraps | Adds or changes |
|---|---|---|
Root | Flex | separator?: boolean draws a line underneath. direction="column", gap="4" |
Banner | Box | src: string, alt?: string, height?: string ('200px'). A cover image with rounded corners |
Main | Flex | layout?: Responsive<'inline' | 'stacked'> ('inline'). align="center", justify="between", gap="4" |
Media | Avatar | type?: 'logo' | 'avatar' ('avatar'), overlap?: boolean. size="6" |
Content | Flex | direction="column", gap="2", flexGrow="1" |
Meta | Flex | gap="2", align="center", wrap="wrap" |
Title | Heading | as="h1", size="9", weight="medium" |
Description | Text | size="4", emphasis="medium" |
Actions | Flex | gap="2", align="center" |
Tabs | Flex | direction="column", width="100%" |
Media with type="logo" is square-cornered; avatar is round. overlap pulls an avatar up over a Banner, and does nothing without one.
Examples
Project Overview
Website redesign
New marketing site, docs and pricing page.<PageHeader.Root>
<PageHeader.Main
layout={{ initial: 'stacked', sm: 'inline' }}
align={{ initial: 'start', sm: 'center' }}
>
<PageHeader.Content>
<PageHeader.Meta>
<Badge color="green" variant="soft">Active</Badge>
<Text size="2" emphasis="medium">Updated 2 hours ago</Text>
</PageHeader.Meta>
<PageHeader.Title as="h3" size="7">Website redesign</PageHeader.Title>
<PageHeader.Description size="3">
New marketing site, docs and pricing page.
</PageHeader.Description>
</PageHeader.Content>
<PageHeader.Actions>
<Button variant="soft" color="gray">Share</Button>
<Button variant="classic">
<HugeiconsIcon icon={Add01Icon} strokeWidth={1.75} />
Add task
</Button>
</PageHeader.Actions>
</PageHeader.Main>
<PageHeader.Tabs>
<Tabs.Root defaultValue="tasks">
<Tabs.List>
<Tabs.Trigger value="tasks">Tasks</Tabs.Trigger>
<Tabs.Trigger value="files">Files</Tabs.Trigger>
<Tabs.Trigger value="activity">Activity</Tabs.Trigger>
</Tabs.List>
</Tabs.Root>
</PageHeader.Tabs>
</PageHeader.Root>Profile With Banner
Maya Rao
Product designer · Berlin<PageHeader.Root>
<PageHeader.Banner src="/cover.jpg" alt="" height="140px" />
<PageHeader.Main align="end" px="4">
<PageHeader.Media overlap fallback="MR" size="7" />
<PageHeader.Content>
<PageHeader.Title as="h3" size="6">Maya Rao</PageHeader.Title>
<PageHeader.Description size="2">
Product designer · Berlin
</PageHeader.Description>
</PageHeader.Content>
<PageHeader.Actions>
<Button variant="classic">Follow</Button>
</PageHeader.Actions>
</PageHeader.Main>
</PageHeader.Root>Accessibility
Title renders an h1 by default; pass as="h2" when the header sits below another h1. Banner has role="img" and uses alt as its label; pass an empty alt when the image is decorative.
© 2026 Kushagra Dhawan. Licensed under MIT. GitHub.