UX Standards — 13: Footer
Governs: The universal footer bar at the bottom of every authenticated page.
Parent rules: See 00-OVERVIEW-AND-CSS-RULES.md first.
Visual Anatomy
┌──────────────────────────────────────────────────────────────────────────┐
│ h-12 (48px) │ border-t │ bg-background │ px-6 │
│ │
│ © 2025 Anshin Health Solutions, Inc. All rights reserved. v1.0.0 • Privacy Policy • Terms of Use • Accessibility │
│ LEFT RIGHT │
└──────────────────────────────────────────────────────────────────────────┘
Footer Container
<footer className="flex h-12 shrink-0 items-center justify-between border-t bg-background px-6 text-sm text-muted-foreground">
| Property | Value | Reason |
|---|---|---|
h-12 | 48px exactly | Non-negotiable |
shrink-0 | Never compressed | Stays 48px regardless of content |
border-t | 1px top border | Visual zone separation |
bg-background | Theme-aware | Works in dark/light mode |
px-6 | 24px horizontal | Slightly wider than header's 16px |
text-sm | 14px | Caption-level text |
text-muted-foreground | Secondary gray | Non-primary UI element |
justify-between | Splits left/right | Copyright left, links right |
Left Side — Copyright
<span>
© {copyrightYear} {companyName} {t('footer.allRightsReserved')}
</span>
| Element | Value |
|---|---|
| Symbol | © (©) |
| Year | Dynamic: new Date().getFullYear() |
| Company | 'Anshin Health Solutions, Inc.' |
| Text | All rights reserved. |
Full text: © 2025 Anshin Health Solutions, Inc. All rights reserved.
Right Side — Version and Policy Links
<div className="flex items-center gap-4">
<span>{version}</span>
{visibleLinks.map((link, index) => (
<React.Fragment key={link.type}>
<span>•</span>
<button
type="button"
onClick={() => openPolicyWindow(link.path)}
className="cursor-pointer border-none bg-transparent p-0 hover:text-primary"
>
{t(link.labelKey)}
</button>
</React.Fragment>
))}
</div>
Default right-side content:
v1.0.0 • Privacy Policy • Terms of Use • Accessibility
| Item | Type | Value |
|---|---|---|
| Version | <span> | v{APP_VERSION} |
| Separator | • (•) | Between version and links, and between links |
| Privacy Policy | <button> | Opens openPersistentPopup('policy', '/policies/privacy', 'policy-window') |
| Terms of Use | <button> | Opens openPersistentPopup('policy', '/policies/terms', 'policy-window') |
| Accessibility | <button> | Opens openPersistentPopup('policy', '/policies/accessibility', 'policy-window') |
Policy links are <button> elements, not <a> tags. They open persistent popup windows, not new tabs.
Link Hover State
className="cursor-pointer border-none bg-transparent p-0 hover:text-primary"
On hover, policy link text changes to text-primary (brand green). No underline, no border.
Footer Component API
// packages/shared/components/layouts/footer.tsx
<Footer
version="1.0.0" // optional, defaults to APP_VERSION
copyrightYear={2025} // optional, defaults to current year
companyName="Anshin Health Solutions" // optional, defaults to full name
excludeLinks={['accessibility']} // optional, removes specific links
/>
| Prop | Type | Default |
|---|---|---|
version | string | '1.0.0' |
copyrightYear | number | new Date().getFullYear() |
companyName | string | 'Anshin Health Solutions, Inc.' |
excludeLinks | string[] | [] (all links shown) |
Accelerate Modules Footer
Accelerate modules do not use the shared Footer component (which is Orchestrate-specific). However, they may show a minimal footer for legal compliance:
// Minimal footer for Accelerate modules (optional, shown on desktop only)
<footer className="hidden lg:flex h-12 shrink-0 items-center justify-between border-t bg-background px-6 text-sm text-muted-foreground">
<span>© {new Date().getFullYear()} Anshin Health Solutions, Inc.</span>
<span className="text-xs">{APP_RELEASE_VERSION}</span>
</footer>
If legal links are required, use the same persistent popup pattern.
Mobile Behavior
On mobile (< lg), the footer is hidden by default:
// Mobile: hidden. Desktop: visible.
<footer className="hidden lg:flex h-12 ...">
The mobile header carries the notification bell and app name. Footer content is not critical enough for mobile viewport.
Positioning Within Layout — CRITICAL
The footer is always the last element inside SidebarInset (the right column). It does NOT span the sidebar. The sidebar user card and the footer bottom-align at the same level naturally because they are both at the bottom of their respective columns.
Orchestrate (SidebarProvider layout) — CORRECT
<SidebarProvider>
<AppSidebar /> {/* Full-height left rail */}
<SidebarInset> {/* Right column — flex col */}
<header className="sticky top-0 z-50 h-16 ...">...</header>
<main className="flex-1 overflow-y-auto p-6">{children}</main>
<footer className="h-12 shrink-0 ...">...</footer> {/* ← Inside right column */}
</SidebarInset>
</SidebarProvider>
What this looks like
┌─────────────────────────────────────────────────────────┐
│ SidebarProvider │
│ ┌──────────────┬──────────────────────────────────┐ │
│ │ │ <header> h-16 sticky │ │
│ │ AppSidebar ├──────────────────────────────────┤ │
│ │ (full │ <main> flex-1 overflow-y-auto │ │
│ │ height) │ │ │
│ │ ├──────────────────────────────────┤ │
│ │ [UserCard] │ <footer> h-12 ← same level │ │
│ └──────────────┴──────────────────────────────────┘ │
└─────────────────────────────────────────────────────────┘
The sidebar's SidebarUser card and the footer naturally align at the same visual height because both are at the bottom of their full-height columns.
❌ WRONG — footer at root level (spans sidebar)
// NEVER do this — footer would span the full page width including over the sidebar
<div className="min-h-screen w-full flex flex-col">
<div className="flex flex-1">
<Sidebar />
<main className="flex-1">{children}</main>
</div>
<Footer /> {/* ← WRONG: at root level, spans sidebar */}
</div>
Accelerate Modules (custom aside layout) — CORRECT
<div className="flex h-screen overflow-hidden">
<aside className="hidden lg:flex flex-col w-64 border-r bg-card shrink-0">
{/* sidebar content */}
</aside>
<div className="flex flex-col flex-1 overflow-hidden">
<header className="sticky top-0 h-16 ...">...</header>
<main className="flex-1 overflow-y-auto p-6">{children}</main>
<footer className="hidden lg:flex h-12 ...">...</footer>
</div>
</div>
i18n Keys
// public/locales/en/common.json
{
"footer": {
"allRightsReserved": "All rights reserved.",
"version": "v{{version}}",
"privacyPolicy": "Privacy Policy",
"termsOfUse": "Terms of Use",
"accessibility": "Accessibility"
}
}
Violation Checklist
- Footer is inside
SidebarInset(right column) — NOT at root level spanning sidebar - Footer uses
h-12 shrink-0 border-t bg-background px-6 - Text is
text-sm text-muted-foreground -
justify-betweenfor left/right split - Copyright text on left:
© {year} {company} All rights reserved. - Version and policy links on right with
•separators - Policy links are
<button>elements (not<a>tags) - Policy links use
openPersistentPopup()— neverwindow.open()directly - Policy links hover to
hover:text-primary - Footer is
hidden lg:flexon mobile - Footer is the last element inside
SidebarInset(not last element of the root layout)