Source: https://monarchui.dev/components/record-header/

Patterns · CRM

# RecordHeader

The header of a record detail page: avatar, editable title, status, key facts, actions, prev/next, state banner and tabs.

@rookcom/monarch-react/patterns

Pattern

## Contact with facts, actions and tabs

## Usage examples

### Ticket with editable subject and prev/next

### Record state banner

## Purpose

Heads the detail page of one record (a contact, a ticket, a project): a back link and previous/next navigation, an avatar or icon, the title (editable in place), status and badges, a subtitle, key facts, record actions, a state banner (archived, merged) and section tabs. The product supplies all data and callbacks.

### Use when

- The detail page or detail pane of one record.
- People move through a list record by record (prev/next with a position).
- The record’s name can be renamed in place.

### Avoid when

- A page that is not about one record (a list, settings, a dashboard): use PageHeader.
- Facts that are the main content of the page: use a DescriptionList in the body.

## Design principles

- Identity first: avatar, name and status read as one block.
- Key facts only: three to six facts that orient, not the whole record.
- State is visible: archived, merged or deleted records say so in the banner, under the identity.
- No business logic: the product decides which actions, facts and neighbours exist.

## API reference

```tsx
import { RecordHeader } from '@rookcom/monarch-react/patterns';
```

| Export | Kind | Rendering | Root class |
| --- | --- | --- | --- |
| `RecordHeader` | component | server-safe | `.mn-record-header` |

Renders a `<header>`, forwards its ref and passes native attributes through.

With `onTitleSave` and a string `title`, the title is an InlineEditText inside a `role="heading"` element (a block editor may not sit in an `<h1>`). `titleEditProps` passes validate, maxLength and the rest through.

`facts` render as a DescriptionList with `columns="auto"` (9rem minimum); `copyable` and empty placeholders work as there.

`navigator` is `{ position?, onPrevious?, onNext?, previousLabel?, nextLabel?, formatPosition? }`; a missing handler disables its button. Keyboard shortcuts such as J/K belong to the product.

Exported from `@rookcom/monarch-react/patterns`.

Generated from the TypeScript declarations of `packages/react/src/patterns/crm/record-header`.

## Properties

### `RecordHeader`

The header of a record detail page: back link and prev/next navigation, avatar, title (optionally editable), status and badges, key facts, actions, a state banner and section tabs. The product supplies the data.

| Prop | Type | Description |
| --- | --- | --- |
| `title` required | `ReactNode` | The record's name: a contact, ticket subject or project name. The page heading. |
| `actions` | `ReactNode` | Record actions: Buttons and a DropdownMenu ("More actions for Ada Lovelace"). |
| `avatar` | `ReactNode` | An Avatar, or an icon tile for records without a picture. Decorative unless it has its own name. |
| `back` | `ReactNode` | A BackLink or Breadcrumb above the identity block. |
| `badges` | `ReactNode` | More badges after the status (priority, tier, "Live chat"). |
| `banner` | `ReactNode` | A record state notice under the header content: archived, merged, deleted (an InlineNotice or Alert). |
| `children` | `ReactNode` |  |
| `className` | `string` |  |
| `facts` | `RecordHeaderFact[]` | Key facts under the identity block, as a compact DescriptionList. |
| `headingLevel` | `"h2" \| "h1"` | Heading level of the title. `h1` (default) on a record page; `h2` in an inbox detail pane. |
| `id` | `string` |  |
| `navigator` | `RecordHeaderNavigator` | Previous/next record buttons at the top end. |
| `onTitleSave` | `((next: string) => void \| Promise<void>)` | Makes a text `title` editable in place with InlineEditText. Resolve to save; reject to keep the editor open with the error. |
| `status` | `ReactNode` | A StatusBadge next to the title. |
| `subtitle` | `ReactNode` | A line under the title: the record number, email, "Opened 3 days ago by Ada". |
| `tabs` | `ReactNode` | A TabsList or TabsNav for the record's sections. |
| `titleEditProps` | `Partial<Omit<InlineEditTextProps, "value" \| "defaultValue" \| "label" \| "onSave">>` | Extra InlineEditText props for the title editor (validate, maxLength, disabled). |
| `titleLabel` | `string` | Accessible name of the title editor. Default "Title". |

## Variants

| Prop | Values | Description |
| --- | --- | --- |
| `avatar / status / badges / subtitle / facts / actions / back / navigator / banner / tabs` | `'omitted'``'provided'` | Each part renders only when provided. |
| `headingLevel` | `'h1'``'h2'` | `h1` (default) on a record page; `h2` inside an inbox detail pane under the page h1. |
| `onTitleSave` | `'omitted'``'function'` | Makes a text title editable in place. Exposed as `data-editable`. |

## States

| State | Exposed as | Behaviour |
| --- | --- | --- |
| Static | – | Without `onTitleSave` and `navigator` the header has no state of its own. |
| Editing title | – | InlineEditText states: editing, saving (spinner), error (kept open with the message). |
| First / last record | – | The previous or next button is disabled when its handler is omitted. |

## Interaction behaviour

- Activating the title (when editable) opens the editor; Enter or blur saves, Escape cancels.
- Previous and next call `onPrevious` and `onNext`; the product navigates and updates `position`.
- Fact copy buttons copy their value.

## Keyboard support

| Keys | Action |
| --- | --- |
| Tab | Back link, previous/next, title editor, actions, fact copy buttons, banner actions, tabs. |
| Enter | On the title, starts editing; in the editor, saves. |
| Escape | In the title editor, cancels and restores the title. |

## Accessibility

Role

banner landmark only when it is not inside main, article or section (native <header> rules)

Accessible name

The title is the page or pane heading.

- The title is an `h1` (or `h2`); an editable title keeps the heading role and level.
- Previous/next are icon buttons with required names; name the record type ("Next ticket").
- Put role="status" content in `banner` (InlineNotice) so state changes are announced.
- Give an actions menu a specific name: "More actions for Ada Lovelace".
- The live examples on this page render with `headingLevel="h2"` because the docs page has its own h1; in an app use the default h1.

## Responsive behaviour

- The identity block and the actions wrap; actions take the full width under 480px.
- Facts fill as many 9rem columns as fit and become one column at 640px and below.
- Long titles wrap anywhere; the title steps down a size under 480px.
- Actions, navigator and tabs are hidden when printing.

## Theme support

- Text, borders and dividers use Monarch semantic tokens, so every theme applies.

## Do and don't

### Do

- Keep facts to the few that orient someone on arrival.
- Name previous/next after the record type.
- Show archived, merged and deleted states in `banner`.

### Don't

- Do not put a second h1 on a page that has a RecordHeader.
- Do not make a rich (non-text) title editable; `onTitleSave` needs a string title.
- Do not repeat every field of the record in `facts`.

## Related components

- [PageHeader](https://monarchui.dev/components/page-header/): Headers for pages that are not one record.
- [DescriptionList](https://monarchui.dev/components/description-list/): The facts row; use it in the body for the full record.
- [InlineEditText](https://monarchui.dev/components/inline-edit-text/): The editable title.
- [SplitInboxLayout](https://monarchui.dev/components/split-inbox-layout/): Hosts a RecordHeader (h2) in its detail pane.
- [Breadcrumb](https://monarchui.dev/components/breadcrumb/): An alternative to BackLink in `back`.

## Dependencies

| Kind | Depends on |
| --- | --- |
| npm packages | `react` |
| Monarch components | [Button](https://monarchui.dev/components/button/), [DescriptionList](https://monarchui.dev/components/description-list/), [InlineEditText](https://monarchui.dev/components/inline-edit-text/) |

## Testing coverage

- Renders back link, avatar, title (h1), status and badges, subtitle, facts (dl with copy and placeholder), actions, banner and tabs; ref forwarding.
- Prev/next: position text, custom labels, disabled first record, keyboard activation.
- Custom position format; no position for an empty list.
- Editable title keeps the heading role; Enter saves, Escape cancels; extra editor props apply.
- A non-text title stays a plain heading.
- Renders on the server.

| Check | Covered |
| --- | --- |
| Axe in jsdom | Yes |
| Server rendering | Yes |
| Browser suites | Visual baselines: /__gallery/record-header; Cross-browser axe: /components/record-header |

Tests: `packages/react/src/patterns/crm/record-header`

## All example code

### Contact with facts, actions and tabs

```tsx
import { Avatar, BackLink, Badge, Button, DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, IconButton, StatusBadge, TabsLink, TabsNav } from '@rookcom/monarch-react';
import { RecordHeader } from '@rookcom/monarch-react/patterns';

// MoreIcon: demo icon from the docs site; use your own.

<RecordHeader
  headingLevel="h2"
  back={<BackLink href="#/components/record-header">All contacts</BackLink>}
  avatar={<Avatar name="Ada Lovelace" size="lg" />}
  title="Ada Lovelace"
  status={<StatusBadge status="success" label="Active" />}
  badges={<Badge>Enterprise</Badge>}
  subtitle={<span>ada@example.com · Head of Engineering, Northfield</span>}
  facts={[
    { term: 'Owner', value: 'Grace Hopper' },
    { term: 'Lifecycle', value: 'Customer' },
    { term: 'Contact ID', value: 'c_1042', copyable: true },
    { term: 'Phone', value: null },
  ]}
  actions={
    <>
      <Button>Email</Button>
      <DropdownMenu>
        <DropdownMenuTrigger asChild>
          <IconButton
            label="More actions for Ada Lovelace"
            icon={<MoreIcon />}
            variant="secondary"
          />
        </DropdownMenuTrigger>
        <DropdownMenuContent align="end">
          <DropdownMenuItem>Merge</DropdownMenuItem>
          <DropdownMenuItem>Archive</DropdownMenuItem>
          <DropdownMenuItem tone="danger">Delete contact</DropdownMenuItem>
        </DropdownMenuContent>
      </DropdownMenu>
    </>
  }
  tabs={
    <TabsNav aria-label="Contact sections">
      <TabsLink href="#/components/record-header" active>
        Overview
      </TabsLink>
      <TabsLink href="#/components/record-header">Activity</TabsLink>
      <TabsLink href="#/components/record-header">Deals</TabsLink>
    </TabsNav>
  }
/>
```

### Ticket with editable subject and prev/next

```tsx
import { useState } from 'react';
import { BackLink, Badge, StatusBadge } from '@rookcom/monarch-react';
import { RecordHeader } from '@rookcom/monarch-react/patterns';

const tickets = [
  'Printer offline on floor 3',
  'Invoice shows the wrong address',
  'Reset two-factor',
];

function TicketDemo() {
  const [index, setIndex] = useState(0);
  const [subjects, setSubjects] = useState(tickets);
  return (
    <RecordHeader
      headingLevel="h2"
      back={<BackLink href="#/components/record-header">Inbox</BackLink>}
      navigator={{
        position: { index, total: subjects.length },
        previousLabel: 'Previous ticket',
        nextLabel: 'Next ticket',
        ...(index > 0 ? { onPrevious: () => setIndex(index - 1) } : {}),
        ...(index < subjects.length - 1 ? { onNext: () => setIndex(index + 1) } : {}),
      }}
      title={subjects[index] ?? ''}
      titleLabel="Subject"
      onTitleSave={(next) => setSubjects(subjects.map((s, i) => (i === index ? next : s)))}
      status={<StatusBadge status="info" label="Open" />}
      badges={<Badge tone="warning">High</Badge>}
      subtitle={<span>#{1040 + index} · Opened 2 hours ago by Grace Hopper</span>}
    />
  );
}

<TicketDemo />
```

### Record state banner

```tsx
import { Button, InlineNotice, StatusBadge } from '@rookcom/monarch-react';
import { RecordHeader } from '@rookcom/monarch-react/patterns';

<RecordHeader
  headingLevel="h2"
  title="Q3 renewal"
  status={<StatusBadge status="neutral" label="Archived" />}
  subtitle="Northfield · Opportunity"
  banner={
    <InlineNotice tone="neutral">
      Archived on 2 May. It no longer appears in pipelines or reports.
    </InlineNotice>
  }
  actions={<Button>Unarchive</Button>}
/>
```
