Settings page layout
How Cal.com composes authenticated settings pages — shell, AppHeader, and stacked section frames.
Settings pages are the highest-volume authenticated surface in Cal.com. Almost every route under
apps/web/app/(use-page-wrapper)/settings/ follows the same recipe: SettingsShell provides
secondary navigation, then the page content column is AppHeader + stacked CardFrame (or
list) sections.
Field wiring, validation, and save footers are covered in Forms. This Guide is the page chrome around those cards.
import {
AppHeader,
AppHeaderActions,
AppHeaderContent,
AppHeaderDescription,
} from "@coss/ui/shared/app-header";
import { Card, CardFrame, CardFrameFooter, CardPanel } from "@coss/ui/components/card";Product UI detail for the header composite: App Header.
Anatomy
- SettingsShell — route-group layout (
settings/(settings-layout)/layout.tsx→SettingsShellServer). Secondary nav + content column. Do not re-implement this in a page. AppHeader— page title (AppHeaderContent title=…), optional description, optional actions.- Body — one or more sections in a vertical stack (
flex flex-col gap-4), usuallyCardFrame+ form cards; sometimes a list or table under the header. page.tsxcontract — session, data fetching, and permission checks live in the App Router page. UI often lives in~/modules/settings/.... Prefer fail-closednotFound()when the caller lacks permission (never invent a “no access” empty inlayout.tsx).
Best practices
Header · title and description
Most settings views use title + description with no actions.
General
Manage settings for your language and timezone
<AppHeader>
<AppHeaderContent title={t("general")}>
<AppHeaderDescription>{t("general_description")}</AppHeaderDescription>
</AppHeaderContent>
</AppHeader>AppHeaderContent requires the title prop — do not put the heading only as children.
Header · with actions
Put primary create / add CTAs in AppHeaderActions. Hide the action when it does not apply (empty
list CTAs often live in the empty state instead — see Empty states).
Out of office
Schedule absences and automatic booking redirects
<AppHeader>
<AppHeaderContent title={t("out_of_office")}>
<AppHeaderDescription>{headerDescription}</AppHeaderDescription>
</AppHeaderContent>
{showAddEntryAction ? (
<AppHeaderActions>
<CreateNewOutOfOfficeEntryButton onClick={handleOpenCreateDialog} />
</AppHeaderActions>
) : null}
</AppHeader>The same shape appears with the CTA gated by PBAC on org/team roles pages (canCreate →
AppHeaderActions).
Full page recipe
Under the shell content column: header, then stacked cards. Forms use Form className="contents"
so the card chrome owns the layout — details in Forms → Settings page card.
General
Manage settings for your language and timezone
<>
<AppHeader>
<AppHeaderContent title={t("general")}>
<AppHeaderDescription>{t("general_description")}</AppHeaderDescription>
</AppHeaderContent>
</AppHeader>
<div className="flex flex-col gap-4">
<Form className="contents" onSubmit={formMethods.handleSubmit(onSubmit)}>
<CardFrame>
<Card>
<CardPanel>{/* FieldGrid / fields */}</CardPanel>
</Card>
<CardFrameFooter className="flex justify-end">
<Button type="submit" disabled={isDisabled} loading={isSubmitting}>
{t("update")}
</Button>
</CardFrameFooter>
</CardFrame>
</Form>
{/* Additional CardFrame sections as needed */}
</div>
</>Where AppHeader lives
Both placements ship today — pick one and keep the page thin:
| Placement | When |
|---|---|
In the module view (~/settings/...) | Client-heavy pages (General, Out of Office, Profile) |
In page.tsx above the view | Server pages that already have translate + CTAs (Roles) |
Do not put permission checks in layout.tsx. Authorize in page.tsx, then render header +
body (or notFound()).
if (!canRead) {
return notFound();
}
return (
<>
<AppHeader>
<AppHeaderContent title={t("roles_and_permissions")}>
<AppHeaderDescription>{t("roles_and_permissions_description")}</AppHeaderDescription>
</AppHeaderContent>
{canCreate ? (
<AppHeaderActions>
<CreateRoleCTA />
</AppHeaderActions>
) : null}
</AppHeader>
<RolesList ... />
</>
);Not this Guide
Call sites
| File | Notes |
|---|---|
apps/web/app/(use-page-wrapper)/settings/(settings-layout)/layout.tsx | SettingsShell via SettingsShellServer |
apps/web/components/settings-shell/settings-shell-server.tsx | Nav data + shell provider |
apps/web/modules/settings/my-account/general-view.tsx | AppHeader + stacked CardFrames |
apps/web/modules/settings/my-account/out-of-office-view.tsx | AppHeader + conditional actions |
apps/web/app/(use-page-wrapper)/settings/(settings-layout)/organizations/roles/page.tsx | AppHeader in page.tsx + notFound() |
apps/web/modules/webhooks/views/webhooks-header.tsx | Shared settings header + actions slot |
apps/web/modules/settings/my-account/components/profile-form-card.tsx | CardFrame form section under profile header |
Example URLs
Not exhaustive — a few places you can open locally (e.g. http://localhost:3000) to see this in
product.
| Path | Notes |
|---|---|
/settings/my-account/general | Header + stacked form cards |
/settings/my-account/out-of-office | Header + actions + segmented tabs |
/settings/my-account/profile | Profile CardFrame under settings chrome |
/settings/organizations/roles | Header in page.tsx + permission gate |
/settings/developer/webhooks | Header + list CTA |