Skip to main content

UX Standards — 04: Page Title and Subtitle

Governs: How every page declares and renders its title and optional subtitle. Parent rules: See 00-OVERVIEW-AND-CSS-RULES.md first.


Where Titles Live

The page title and subtitle always appear in the Left Zone of the header, never on the page body itself.

┌────────────────────────────────────────────────────────────────┐
│ [Page Title] [Company Logo] [Icons] │
│ [Page Subtitle] │
└────────────────────────────────────────────────────────────────┘

They do not appear as <h1> elements inside the <main> content area. The header IS the page heading.


Visual Spec

// HeaderLeft component — always present
<div className="flex flex-col min-w-0">
<h1 className="text-lg font-semibold leading-none tracking-tight truncate">
{pageTitle}
</h1>
{pageDescription && (
<p className="text-sm text-muted-foreground leading-none mt-0.5 truncate">
{pageDescription}
</p>
)}
</div>

Title

PropertySpec
Element<h1>
Font sizetext-lg (18px)
Font weightfont-semibold
Line heightleading-none
Trackingtracking-tight
Overflowtruncate (never wraps)
Colortext-foreground (default — no class needed)

Subtitle

PropertySpec
Element<p>
ConditionOnly renders when pageDescription is defined
Font sizetext-sm (14px)
Colortext-muted-foreground
Line heightleading-none
Margin topmt-0.5 (2px gap from title)
Overflowtruncate

Content Guide for Subtitles

The subtitle describes what the page manages or what the user does there:

PageGood Subtitle
User ListManage your users and their roles here.
Fax InboxReview and process incoming faxes.
Product SetupConfigure product types, categories, and attributes.
Referral OutreachManage patient referral outreach campaigns.

Keep subtitles to one sentence, under 80 characters, ending with a period.


Pattern 1 — Orchestrate Central Registry

Orchestrate resolves page titles from a central map using the current pathname.

// packages/shared/components/layouts/page-titles.ts

export interface PageTitleConfig {
titleKey: string; // i18n key for the title
descriptionKey?: string; // i18n key for the subtitle (optional)
}

export const pageTitleMap: Record<string, PageTitleConfig> = {
'/master-data/product-setup': {
titleKey: 'masterData.productSetup.title',
descriptionKey: 'masterData.productSetup.description'
},
'/dashboard/users': {
titleKey: 'users.list.title',
descriptionKey: 'users.list.description'
},
'/dashboard': {
titleKey: 'dashboard.title',
descriptionKey: 'dashboard.description'
},
// ... one entry per route
};

Resolution in the Header:

// The header receives the current pathname and resolves the title
const config = getPageTitleConfig(pathname, pageTitleMap);
const pageTitle = t(config.titleKey);
const pageDescription = config.descriptionKey ? t(config.descriptionKey) : undefined;

Rule: Never hardcode a page title inside a page component in Orchestrate. Always register it in pageTitleMap.


Pattern 2 — Dynamic Title Override

For pages where the title depends on data (e.g., a user's name on their detail page), use the override context:

// In a detail page component:
import { usePageTitleOverride } from '@/components/layouts/page-title-override-context';

export default function UserDetailPage({ params }: { params: { id: string } }) {
const { data: user } = useQuery({ queryKey: ['user', params.id], queryFn: fetchUser });
const { setTitle } = usePageTitleOverride();

// Set the header title once data is loaded
useEffect(() => {
if (user) {
setTitle({
title: user.name,
description: `User ID: ${user.id}`
});
}
// Cleanup: reset to route-based title when unmounting
return () => setTitle(null);
}, [user, setTitle]);

return <div>...</div>;
}

The override context has priority over pageTitleMap. When setTitle returns a value, it is shown in the header; otherwise the pathname-based lookup is used.


Pattern 3 — Accelerate Module Constants

Until Accelerate modules adopt a central registry, define title and subtitle as module-level constants at the top of each page component:

// modules/fax-automation/app/web-fax-dashboard/app/(dashboard)/inbox/page.tsx

const PAGE_TITLE = 'Fax Inbox';
const PAGE_SUBTITLE = 'Review and process incoming faxes.';

export default function InboxPage() {
// Pass these to the layout or header
return <DashboardPage title={PAGE_TITLE} subtitle={PAGE_SUBTITLE}>
...
</DashboardPage>
}

Rules for this pattern:

  • Define constants at the top of the file, before any component code
  • Use PAGE_TITLE and PAGE_SUBTITLE as the constant names
  • Pass them as props to the page layout shell — never hardcode inline inside JSX

i18n Translation Keys

For Orchestrate, title text lives in locale files:

// public/locales/en/common.json
{
"masterData": {
"productSetup": {
"title": "Product Setup",
"description": "Configure product types, categories, and attributes."
}
},
"users": {
"list": {
"title": "User List",
"description": "Manage your users and their roles here."
}
}
}

For Accelerate modules (no i18n yet): use string literals in constants.


What NOT to Do

// ❌ WRONG — title as H1 on page body
<main>
<h1 className="text-2xl font-bold mb-4">User List</h1>
...
</main>

// ❌ WRONG — hardcoded in JSX
<Header title="User List" subtitle="Manage users" />

// ❌ WRONG — title absent (header shows nothing)
<DashboardPage>
<DataTable ... />
</DashboardPage>

// ❌ WRONG — subtitle wraps to two lines (no truncate)
<p className="text-sm text-muted-foreground">
This is a very long subtitle that will wrap onto multiple lines and break the header layout.
</p>

Browser Tab Title

The <title> tag for the browser tab is set separately from the header display title. Use Next.js metadata:

// In page.tsx (App Router)
export const metadata: Metadata = {
title: 'User List — Anshin Orchestrate',
};

// Or dynamically:
export async function generateMetadata({ params }): Promise<Metadata> {
const user = await fetchUser(params.id);
return {
title: `${user.name} — Anshin Orchestrate`,
};
}

Violation Checklist

  • Page title is <h1> with text-lg font-semibold leading-none tracking-tight truncate
  • Subtitle is <p> with text-sm text-muted-foreground leading-none mt-0.5 truncate
  • Title appears ONLY in header left zone, not on the page body
  • Subtitle is a single sentence describing what the page manages
  • No hardcoded titles in Orchestrate page components (use pageTitleMap)
  • Accelerate modules define PAGE_TITLE and PAGE_SUBTITLE constants at file top
  • Dynamic titles use usePageTitleOverride() with cleanup on unmount
  • Browser tab title set via Next.js metadata export