Skip to main content

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 className="flex h-12 shrink-0 items-center justify-between border-t bg-background px-6 text-sm text-muted-foreground">
PropertyValueReason
h-1248px exactlyNon-negotiable
shrink-0Never compressedStays 48px regardless of content
border-t1px top borderVisual zone separation
bg-backgroundTheme-awareWorks in dark/light mode
px-624px horizontalSlightly wider than header's 16px
text-sm14pxCaption-level text
text-muted-foregroundSecondary grayNon-primary UI element
justify-betweenSplits left/rightCopyright left, links right

<span>
&copy; {copyrightYear} {companyName} {t('footer.allRightsReserved')}
</span>
ElementValue
Symbol© (&copy;)
YearDynamic: new Date().getFullYear()
Company'Anshin Health Solutions, Inc.'
TextAll rights reserved.

Full text: © 2025 Anshin Health Solutions, Inc. All rights reserved.


<div className="flex items-center gap-4">
<span>{version}</span>
{visibleLinks.map((link, index) => (
<React.Fragment key={link.type}>
<span>&bull;</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

ItemTypeValue
Version<span>v{APP_VERSION}
Separator (&bull;)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.


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.


// 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
/>
PropTypeDefault
versionstring'1.0.0'
copyrightYearnumbernew Date().getFullYear()
companyNamestring'Anshin Health Solutions, Inc.'
excludeLinksstring[][] (all links shown)

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>&copy; {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.

// 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-between for 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() — never window.open() directly
  • Policy links hover to hover:text-primary
  • Footer is hidden lg:flex on mobile
  • Footer is the last element inside SidebarInset (not last element of the root layout)