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
| Property | Spec |
|---|---|
| Element | <h1> |
| Font size | text-lg (18px) |
| Font weight | font-semibold |
| Line height | leading-none |
| Tracking | tracking-tight |
| Overflow | truncate (never wraps) |
| Color | text-foreground (default — no class needed) |
Subtitle
| Property | Spec |
|---|---|
| Element | <p> |
| Condition | Only renders when pageDescription is defined |
| Font size | text-sm (14px) |
| Color | text-muted-foreground |
| Line height | leading-none |
| Margin top | mt-0.5 (2px gap from title) |
| Overflow | truncate |
Content Guide for Subtitles
The subtitle describes what the page manages or what the user does there:
| Page | Good Subtitle |
|---|---|
| User List | Manage your users and their roles here. |
| Fax Inbox | Review and process incoming faxes. |
| Product Setup | Configure product types, categories, and attributes. |
| Referral Outreach | Manage 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_TITLEandPAGE_SUBTITLEas 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>withtext-lg font-semibold leading-none tracking-tight truncate - Subtitle is
<p>withtext-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_TITLEandPAGE_SUBTITLEconstants at file top - Dynamic titles use
usePageTitleOverride()with cleanup on unmount - Browser tab title set via Next.js
metadataexport