Page Header

Title, description, actions and tabs for a page

Page 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-ui

Copy this file into components/blocks/page-header. Keep the folder names, since blocks import each other by relative path.

components/blocks/page-header/page-header.tsx
'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.

PartWrapsAdds or changes
RootFlexseparator?: boolean draws a line underneath. direction="column", gap="4"
BannerBoxsrc: string, alt?: string, height?: string ('200px'). A cover image with rounded corners
MainFlexlayout?: Responsive<'inline' | 'stacked'> ('inline'). align="center", justify="between", gap="4"
MediaAvatartype?: 'logo' | 'avatar' ('avatar'), overlap?: boolean. size="6"
ContentFlexdirection="column", gap="2", flexGrow="1"
MetaFlexgap="2", align="center", wrap="wrap"
TitleHeadingas="h1", size="9", weight="medium"
DescriptionTextsize="4", emphasis="medium"
ActionsFlexgap="2", align="center"
TabsFlexdirection="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

ActiveUpdated 2 hours ago

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.

Theme

Accent color

Gray color

Appearance

Radius

Scaling

Panel background