Section Header

Title and actions at the top of a section

Section Header introduces a section within a page: a title, an optional description, and actions to the right. On narrow screens it can stack. The examples on every component page here start with one.

Installation

Install the dependencies.

npm install @kushagradhawan/kookie-ui

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

components/blocks/section-header/section-header.tsx
import * as React from 'react';
import { Flex, Heading, Separator, Text } from '@kushagradhawan/kookie-ui';
import type { 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 = 'stacked' | 'inline';

/** Maps a (responsive) layout to a (responsive) Flex prop value. */
function mapLayout<T>(layout: Responsive<Layout>, map: (value: Layout) => T): Responsive<T> {
  if (typeof layout === 'string') return map(layout);

  const result: Partial<Record<Breakpoint, T>> = {};
  for (const [breakpoint, value] of Object.entries(layout) as [Breakpoint, Layout | undefined][]) {
    if (value) result[breakpoint] = map(value);
  }
  return result;
}

type SectionHeaderRootProps = Omit<FlexProps, 'align'> & {
  /** `inline` puts actions to the right of the title; `stacked` puts them below. @default "inline" */
  layout?: Responsive<Layout>;
  /** Cross-axis alignment. Defaults to `center` for inline and `start` for stacked. */
  align?: Responsive<'start' | 'center'>;
  /** Draw a separator under the header. */
  separator?: boolean;
};

const SectionHeaderRoot = React.forwardRef<HTMLDivElement, SectionHeaderRootProps>(
  ({ layout = 'inline', align, separator = false, gap = '4', direction, children, ...props }, ref) => (
    <Flex ref={ref} direction="column" gap={gap} {...props}>
      <Flex
        direction={direction ?? mapLayout(layout, (value) => (value === 'inline' ? 'row' : 'column'))}
        justify={mapLayout(layout, (value) => (value === 'inline' ? 'between' : 'start'))}
        align={align ?? mapLayout(layout, (value) => (value === 'inline' ? 'center' : 'start'))}
        gap={gap}
        wrap="wrap"
      >
        {children}
      </Flex>
      {separator && <Separator size="4" />}
    </Flex>
  ),
);
SectionHeaderRoot.displayName = 'SectionHeader.Root';

const SectionHeaderContent = React.forwardRef<HTMLDivElement, FlexProps>(
  ({ direction = 'column', gap = '1', ...props }, ref) => <Flex ref={ref} direction={direction} gap={gap} {...props} />,
);
SectionHeaderContent.displayName = 'SectionHeader.Content';

const SectionHeaderTitle = React.forwardRef<HTMLHeadingElement, HeadingProps>(
  ({ as = 'h2', size = '6', weight = 'medium', ...props }, ref) => (
    <Heading ref={ref} as={as} size={size} weight={weight} {...props} />
  ),
);
SectionHeaderTitle.displayName = 'SectionHeader.Title';

const SectionHeaderDescription = React.forwardRef<HTMLParagraphElement, TextProps>(
  ({ size = '3', emphasis = 'medium', ...props }, ref) => <Text ref={ref} size={size} emphasis={emphasis} {...props} />,
);
SectionHeaderDescription.displayName = 'SectionHeader.Description';

const SectionHeaderActions = React.forwardRef<HTMLDivElement, FlexProps>(
  ({ gap = '2', align = 'center', ...props }, ref) => <Flex ref={ref} gap={gap} align={align} {...props} />,
);
SectionHeaderActions.displayName = 'SectionHeader.Actions';

const SectionHeaderTabs = React.forwardRef<HTMLDivElement, FlexProps>(({ width = '100%', ...props }, ref) => (
  <Flex ref={ref} width={width} {...props} />
));
SectionHeaderTabs.displayName = 'SectionHeader.Tabs';

export const SectionHeader = Object.assign(SectionHeaderRoot, {
  Root: SectionHeaderRoot,
  Content: SectionHeaderContent,
  Title: SectionHeaderTitle,
  Description: SectionHeaderDescription,
  Actions: SectionHeaderActions,
  Tabs: SectionHeaderTabs,
});

Usage

import { SectionHeader } from '@/components/blocks/section-header/section-header';
 
<SectionHeader.Root>
  <SectionHeader.Content>
    <SectionHeader.Title>Members</SectionHeader.Title>
    <SectionHeader.Description>People with access to this workspace.</SectionHeader.Description>
  </SectionHeader.Content>
  <SectionHeader.Actions>
    <Button variant="classic">Invite</Button>
  </SectionHeader.Actions>
</SectionHeader.Root>

Anatomy

<SectionHeader.Root>
  <SectionHeader.Content>
    <SectionHeader.Title />
    <SectionHeader.Description />
  </SectionHeader.Content>
  <SectionHeader.Actions />
  <SectionHeader.Tabs />
</SectionHeader.Root>

Props

Every part accepts the props of the component it wraps. Only the additions and changed defaults are listed.

PartWrapsAdds or changes
RootFlexlayout?: Responsive<'inline' | 'stacked'> ('inline'), align?: Responsive<'start' | 'center'>, separator?: boolean. gap="4"
ContentFlexdirection="column", gap="1"
TitleHeadingas="h2", size="6", weight="medium"
DescriptionTextsize="3", emphasis="medium"
ActionsFlexgap="2", align="center"
TabsFlexwidth="100%"

inline puts content and actions on one row, spaced apart and vertically centered. stacked puts actions below the content, aligned to the start. align overrides the vertical alignment either way.

Examples

Settings Section

Notifications

Choose what you hear about and where.
<SectionHeader.Root separator>
  <SectionHeader.Content>
    <SectionHeader.Title as="h3" size="4">Notifications</SectionHeader.Title>
    <SectionHeader.Description size="2">
      Choose what you hear about and where.
    </SectionHeader.Description>
  </SectionHeader.Content>
  <SectionHeader.Actions>
    <Button variant="soft" color="gray" size="2">Reset</Button>
    <Button variant="classic" size="2">Save</Button>
  </SectionHeader.Actions>
</SectionHeader.Root>

Stacked on Mobile

Stack below the sm breakpoint so long descriptions keep their width.

Activity

Everything that changed in the last 30 days.
<SectionHeader.Root layout={{ initial: 'stacked', sm: 'inline' }}>
  <SectionHeader.Content>
    <SectionHeader.Title as="h3" size="4">Activity</SectionHeader.Title>
    <SectionHeader.Description size="2">
      Everything that changed in the last 30 days.
    </SectionHeader.Description>
  </SectionHeader.Content>
  <SectionHeader.Actions>
    <SegmentedControl.Root defaultValue="all" size="2">
      <SegmentedControl.Item value="all">All</SegmentedControl.Item>
      <SegmentedControl.Item value="mine">Mine</SegmentedControl.Item>
    </SegmentedControl.Root>
  </SectionHeader.Actions>
</SectionHeader.Root>

Accessibility

Title renders an h2. Change as to keep heading levels in order, for example as="h3" inside a section that already has an h2.

© 2026 Kushagra Dhawan. Licensed under MIT. GitHub.

Theme

Accent color

Gray color

Appearance

Radius

Scaling

Panel background