Date: 2026-04-29
Project: Credence-Frontend
Decision: HYBRID RESPONSIVE PATTERN (Hamburger Mobile + Horizontal Desktop)
Status: β
APPROVED FOR IMPLEMENTATION
After comprehensive IA analysis of Credence-Frontend (4 pages, flat hierarchy, web application), we have selected a Hybrid Responsive Navigation Pattern:
- Mobile (< 640px): Hamburger menu with slide-in drawer
- Tablet/Desktop (β₯ 640px): Horizontal header navigation (current pattern)
This decision optimizes for:
- Mobile usability (thumb-friendly, accessible)
- Desktop familiarity (preserve working pattern)
- Accessibility compliance (WCAG 2.1 AA)
- Scalability (supports future page additions)
| Criterion | Hamburger Only | Bottom Tab Bar | Hybrid (Selected) | Horizontal Only |
|---|---|---|---|---|
| Mobile Usability | β Good | β Excellent | β Excellent | β Poor |
| Desktop Usability | β Awkward | β Excellent | β Excellent | |
| Discoverability | β Always visible | β Contextual | β Always visible | |
| Screen Count (3 pages) | β Scales well | β Perfect fit | β Perfect fit | β Works |
| Thumb Reachability | β Bottom (ideal) | β Top-left mobile | β Top (hard) | |
| Web Conventions | β Standard | β Standard | β Standard | |
| Accessibility | β WCAG compliant | β WCAG compliant | β WCAG compliant | |
| Implementation Effort | π‘ Medium | π‘ Medium | π Medium-High | π’ Low (current) |
| Maintenance | π’ Single pattern | π’ Single pattern | π‘ Two patterns | π’ Single pattern |
| Gesture Conflicts | β None | β None | ||
| Future Scalability | β 10+ pages | β 10+ pages |
- Hamburger Only: 7/11 β (good mobile, awkward desktop)
- Bottom Tab Bar: 8/11 β (excellent mobile, uncommon on web)
- Hybrid (Selected): 10/11 β β (best of both worlds)
- Horizontal Only: 5/11
β οΈ (current state, mobile broken)
βββββββββββββββββββββββββββ
β β° Credence [Theme]β β Hamburger trigger
βββββββββββββββββββββββββββ
β β
β [Page Content] β
β β
βββββββββββββββββββββββββββ
Drawer (when open):
βββββββββββββββββββ
β β Menu β
β β
β π Home β β Active state highlighted
β π Bond β
β β Trust Score β
β β
β βββββββββββββββ β
β [Theme Toggle] β
βββββββββββββββββββ
βββββββββββββββββββββββββββββββββββββββββββββββ
β Credence Home Bond Trust [Theme]β β Horizontal (current)
βββββββββββββββββββββββββββββββββββββββββββββββ
Drawer Container:
- Width: 280px (max 70vw)
- Height: 100vh
- Background: var(--bg-card)
- Border-right: 1px solid var(--border-default)
- Box-shadow: 4px 0 12px rgba(0,0,0,0.15)
- Z-index: 1000
- Position: fixed, left: 0, top: 0
Backdrop:
- Background: rgba(0,0,0,0.5)
- Z-index: 999
- Position: fixed, covers viewport
- Backdrop-filter: blur(2px) (optional)
Animation:
- Transition: transform 300ms ease-in-out
- Transform: translateX(-100%) β translateX(0)Link Item:
- Padding: 12px 16px
- Font-size: 16px
- Font-weight: 600
- Border-radius: 8px
- Margin-bottom: 4px
States:
- Default: color var(--text-primary)
- Hover: background var(--bg-page)
- Active: background var(--color-primary), color white
- Focus: outline 2px solid var(--color-primary), offset 2pxButton:
- Size: 44Γ44px (touch target)
- Icon: 24Γ24px (β° three horizontal lines)
- Color: var(--text-primary)
- Background: transparent
- Border: none
- Border-radius: 8px
States:
- Hover: background var(--bg-page)
- Active: scale 0.95
- Focus: outline 2px solid var(--color-primary)Current horizontal navigation in header works well for tablet/desktop. No changes required.
<button
aria-label="Open navigation menu"
aria-expanded={isOpen}
aria-controls="mobile-nav-drawer"
onClick={toggleDrawer}
>
β°
</button><nav id="mobile-nav-drawer" aria-label="Mobile navigation" role="navigation" hidden={!isOpen}>
{/* Navigation links */}
</nav><button aria-label="Close navigation menu" onClick={closeDrawer}>
β
</button>| Key | Action |
|---|---|
| Tab | Move focus through drawer items |
| Shift+Tab | Move focus backward |
| Enter/Space | Activate focused link/button |
| Escape | Close drawer, return focus to hamburger |
- Open drawer: Focus moves to close button (β)
- Tab navigation: Focus trapped within drawer
- Close drawer: Focus returns to hamburger button
- Backdrop click: Close drawer, restore focus
User taps hamburger:
β "Navigation menu, expanded"
User navigates to link:
β "Home, link, current page" (if active)
β "Bond, link" (if not active)
User closes drawer:
β "Navigation menu, collapsed"
π Navigation
π Mobile
π¨ Hamburger Button
βββ Default
βββ Hover
βββ Active
βββ Focus
π¨ Drawer
βββ Closed (hidden)
βββ Open
βββ Header (close button)
βββ Navigation Links
βββ Footer (theme toggle)
π¨ Backdrop
π Desktop
π¨ Horizontal Nav (existing)
{
"mobile-nav": {
"drawer-width": "280px",
"drawer-max-width": "70vw",
"backdrop-opacity": "0.5",
"animation-duration": "300ms",
"animation-easing": "ease-in-out",
"z-index-backdrop": "999",
"z-index-drawer": "1000"
},
"touch-targets": {
"minimum-size": "44px",
"icon-size": "24px",
"padding": "12px 16px"
},
"spacing": {
"drawer-padding": "24px 16px",
"link-margin": "4px",
"header-padding": "16px"
}
}/* Mobile: Hamburger drawer */
@media (max-width: 639px) {
.desktop-nav {
display: none;
}
.mobile-nav {
display: block;
}
}
/* Tablet/Desktop: Horizontal nav */
@media (min-width: 640px) {
.mobile-nav {
display: none;
}
.desktop-nav {
display: flex;
}
}src/components/navigation/
βββ MobileNav.tsx β Hamburger + drawer (mobile only)
βββ DesktopNav.tsx β Horizontal nav (tablet/desktop)
βββ NavigationLinks.tsx β Shared link data
βββ useMediaQuery.ts β Breakpoint detection hook
// src/components/navigation/NavigationLinks.tsx
export const navigationLinks = [
{ path: '/', label: 'Home', icon: 'π ' },
{ path: '/bond', label: 'Bond', icon: 'π' },
{ path: '/trust', label: 'Trust Score', icon: 'β' },
]// src/components/navigation/useMediaQuery.ts
import { useState, useEffect } from 'react'
export function useMediaQuery(query: string): boolean {
const [matches, setMatches] = useState(false)
useEffect(() => {
const media = window.matchMedia(query)
setMatches(media.matches)
const listener = (e: MediaQueryListEvent) => setMatches(e.matches)
media.addEventListener('change', listener)
return () => media.removeEventListener('change', listener)
}, [query])
return matches
}// src/components/navigation/MobileNav.tsx
import { useState, useEffect, useRef } from 'react'
import { Link, useLocation } from 'react-router-dom'
import { navigationLinks } from './NavigationLinks'
import './MobileNav.css'
export default function MobileNav() {
const [isOpen, setIsOpen] = useState(false)
const location = useLocation()
const closeButtonRef = useRef<HTMLButtonElement>(null)
const hamburgerRef = useRef<HTMLButtonElement>(null)
// Close drawer on route change
useEffect(() => {
setIsOpen(false)
}, [location.pathname])
// Focus management
useEffect(() => {
if (isOpen) {
closeButtonRef.current?.focus()
}
}, [isOpen])
// Keyboard support (Escape to close)
useEffect(() => {
const handleEscape = (e: KeyboardEvent) => {
if (e.key === 'Escape' && isOpen) {
setIsOpen(false)
hamburgerRef.current?.focus()
}
}
document.addEventListener('keydown', handleEscape)
return () => document.removeEventListener('keydown', handleEscape)
}, [isOpen])
// Prevent body scroll when drawer open
useEffect(() => {
if (isOpen) {
document.body.style.overflow = 'hidden'
} else {
document.body.style.overflow = ''
}
return () => {
document.body.style.overflow = ''
}
}, [isOpen])
return (
<>
{/* Hamburger Button */}
<button
ref={hamburgerRef}
className="mobile-nav-hamburger"
aria-label="Open navigation menu"
aria-expanded={isOpen}
aria-controls="mobile-nav-drawer"
onClick={() => setIsOpen(true)}
>
β°
</button>
{/* Backdrop */}
{isOpen && (
<div className="mobile-nav-backdrop" onClick={() => setIsOpen(false)} aria-hidden="true" />
)}
{/* Drawer */}
<nav
id="mobile-nav-drawer"
className={`mobile-nav-drawer ${isOpen ? 'open' : ''}`}
aria-label="Mobile navigation"
hidden={!isOpen}
>
<div className="mobile-nav-header">
<button
ref={closeButtonRef}
className="mobile-nav-close"
aria-label="Close navigation menu"
onClick={() => {
setIsOpen(false)
hamburgerRef.current?.focus()
}}
>
β
</button>
</div>
<ul className="mobile-nav-links">
{navigationLinks.map((link) => (
<li key={link.path}>
<Link
to={link.path}
className={`mobile-nav-link ${location.pathname === link.path ? 'active' : ''}`}
aria-current={location.pathname === link.path ? 'page' : undefined}
>
<span className="mobile-nav-icon">{link.icon}</span>
<span>{link.label}</span>
</Link>
</li>
))}
</ul>
</nav>
</>
)
}// src/components/navigation/DesktopNav.tsx
import { Link, useLocation } from 'react-router-dom'
import { navigationLinks } from './NavigationLinks'
import './DesktopNav.css'
export default function DesktopNav() {
const location = useLocation()
return (
<nav aria-label="Main navigation" className="desktop-nav">
{navigationLinks.map((link) => (
<Link
key={link.path}
to={link.path}
className={`desktop-nav-link ${location.pathname === link.path ? 'active' : ''}`}
aria-current={location.pathname === link.path ? 'page' : undefined}
>
{link.label}
</Link>
))}
</nav>
)
}// src/components/Layout.tsx
import { Outlet, Link } from 'react-router-dom'
import ThemeToggle from './ThemeToggle'
import MobileNav from './navigation/MobileNav'
import DesktopNav from './navigation/DesktopNav'
import { useMediaQuery } from './navigation/useMediaQuery'
export default function Layout() {
const isMobile = useMediaQuery('(max-width: 639px)')
return (
<div style={{ minHeight: '100vh', display: 'flex', flexDirection: 'column' }}>
<a className="skip-link" href="#main-content">
Skip to main content
</a>
<header className="app-header">
{isMobile ? <MobileNav /> : null}
<Link to="/" className="app-logo">
Credence
</Link>
{!isMobile ? <DesktopNav /> : null}
<ThemeToggle />
</header>
<main id="main-content" className="app-main">
<Outlet />
</main>
<footer className="app-footer">{/* Footer content */}</footer>
</div>
)
}/* src/components/navigation/MobileNav.css */
/* Hamburger Button */
.mobile-nav-hamburger {
width: 44px;
height: 44px;
display: flex;
align-items: center;
justify-content: center;
font-size: 24px;
background: transparent;
border: none;
border-radius: 8px;
color: var(--text-primary);
cursor: pointer;
transition: background 150ms ease;
}
.mobile-nav-hamburger:hover {
background: var(--bg-page);
}
.mobile-nav-hamburger:active {
transform: scale(0.95);
}
.mobile-nav-hamburger:focus-visible {
outline: 2px solid var(--color-primary);
outline-offset: 2px;
}
/* Backdrop */
.mobile-nav-backdrop {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.5);
z-index: 999;
animation: fadeIn 300ms ease-in-out;
}
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
/* Drawer */
.mobile-nav-drawer {
position: fixed;
top: 0;
left: 0;
width: 280px;
max-width: 70vw;
height: 100vh;
background: var(--bg-card);
border-right: 1px solid var(--border-default);
box-shadow: 4px 0 12px rgba(0, 0, 0, 0.15);
z-index: 1000;
transform: translateX(-100%);
transition: transform 300ms ease-in-out;
overflow-y: auto;
}
.mobile-nav-drawer.open {
transform: translateX(0);
}
.mobile-nav-drawer[hidden] {
display: none;
}
/* Drawer Header */
.mobile-nav-header {
display: flex;
justify-content: flex-end;
padding: 16px;
border-bottom: 1px solid var(--border-default);
}
.mobile-nav-close {
width: 44px;
height: 44px;
display: flex;
align-items: center;
justify-content: center;
font-size: 24px;
background: transparent;
border: none;
border-radius: 8px;
color: var(--text-primary);
cursor: pointer;
transition: background 150ms ease;
}
.mobile-nav-close:hover {
background: var(--bg-page);
}
.mobile-nav-close:focus-visible {
outline: 2px solid var(--color-primary);
outline-offset: 2px;
}
/* Navigation Links */
.mobile-nav-links {
list-style: none;
padding: 24px 16px;
margin: 0;
}
.mobile-nav-link {
display: flex;
align-items: center;
gap: 12px;
padding: 12px 16px;
margin-bottom: 4px;
font-size: 16px;
font-weight: 600;
color: var(--text-primary);
text-decoration: none;
border-radius: 8px;
transition: background 150ms ease;
}
.mobile-nav-link:hover {
background: var(--bg-page);
}
.mobile-nav-link.active {
background: var(--color-primary);
color: white;
}
.mobile-nav-link:focus-visible {
outline: 2px solid var(--color-primary);
outline-offset: 2px;
}
.mobile-nav-icon {
font-size: 20px;
}
/* Responsive: Hide on desktop */
@media (min-width: 640px) {
.mobile-nav-hamburger,
.mobile-nav-backdrop,
.mobile-nav-drawer {
display: none;
}
}/* src/components/navigation/DesktopNav.css */
.desktop-nav {
display: flex;
gap: 1rem;
align-items: center;
}
.desktop-nav-link {
padding: 0.5rem 1rem;
font-weight: 500;
color: var(--text-primary);
text-decoration: none;
border-radius: 8px;
transition: background 150ms ease;
}
.desktop-nav-link:hover {
background: var(--bg-page);
}
.desktop-nav-link.active {
font-weight: 600;
color: var(--color-primary);
}
.desktop-nav-link:focus-visible {
outline: 2px solid var(--color-primary);
outline-offset: 2px;
}
/* Responsive: Hide on mobile */
@media (max-width: 639px) {
.desktop-nav {
display: none;
}
}- Hamburger button opens drawer on mobile
- Drawer slides in from left with animation
- Backdrop closes drawer when clicked
- Close button (β) closes drawer
- Escape key closes drawer
- Active page is highlighted in drawer
- Drawer closes automatically on navigation
- Desktop nav shows on tablet/desktop (β₯ 640px)
- Mobile nav shows on mobile (< 640px)
- Hamburger has
aria-labelandaria-expanded - Drawer has
idandaria-label - Close button has
aria-label - Active link has
aria-current="page" - Focus moves to close button when drawer opens
- Focus returns to hamburger when drawer closes
- Tab navigation trapped in drawer
- Escape key closes drawer
- Screen reader announces drawer state
- All touch targets β₯ 44Γ44px
- Drawer width 280px (max 70vw)
- Backdrop opacity 0.5
- Animation duration 300ms
- Active state uses primary color
- Hover states visible
- Focus outlines visible
- Dark mode compatible
- Icons render correctly
- iOS Safari (iPhone 12, 14, 15 Pro)
- Android Chrome (Pixel, Samsung)
- Desktop Chrome (resize to mobile)
- Desktop Firefox (resize to mobile)
- Desktop Safari (resize to mobile)
- No layout shift on load
- Smooth animation (60fps)
- No scroll jank
- Body scroll locked when drawer open
- Create navigation components
- Add media query hook
- Update Layout component
- Add CSS styles
- Test locally
- Manual testing on real devices
- Accessibility audit (VoiceOver, TalkBack)
- Cross-browser testing
- Performance profiling
- Update Figma designs
- Document component API
- Add usage examples
- Update accessibility.md
- Deploy to staging
- QA review
- Deploy to production
- Monitor analytics
- If critical issues detected, revert Layout.tsx to previous version
- Desktop navigation unchanged (zero risk)
- Mobile users see desktop nav (degraded but functional)
- Navigation usage: 80%+ mobile users successfully navigate
- Time to navigate: < 2 seconds from hamburger tap to page load
- Error rate: < 1% (drawer fails to open/close)
- Accessibility score: 100% WCAG 2.1 AA compliance
- User feedback survey (2 weeks post-launch)
- Accessibility audit report
- Cross-browser compatibility report
- Mobile Navigation Reconnaissance
- Accessibility Guidelines
- Focus Management Patterns
- Figma Design Specs
Decision Approved By: [Product/Design Team]
Date: 2026-04-29
Implementation Owner: [Developer Name]
Target Completion: [Date]
Document Status: β
APPROVED FOR IMPLEMENTATION
Next Step: Create Figma mockups β Implement components β Test β Deploy