17 KiB
Mobile-Friendly Sidebar & Responsive Layout Plan
Problem Statement
The current NextAV layout uses a persistent left sidebar (w-64 expanded / w-16 collapsed) alongside the main content area in a flex h-screen container. While this works well on desktop, it is not mobile-friendly for several reasons:
- The sidebar always occupies screen width — on a 375px mobile screen, even the collapsed
w-16(64px) sidebar consumes ~17% of the viewport, leaving only ~311px for content. - No mobile navigation paradigm — there is no hamburger menu, bottom navigation bar, or swipe-to-open drawer pattern. The sidebar is always rendered.
- The collapse toggle is not a mobile pattern — the
ChevronLeft/ChevronRighttoggle on the sidebar is desktop-oriented; mobile users expect a hamburger icon in a top bar or a bottom tab bar. - Content grids are too dense — while
getColumnCount()in the grids already supports 2 columns at<640px, the available width is further reduced by the always-visible sidebar. - No
<meta name="viewport">consideration — the layout doesn't adapt its core shell for touch devices.
Current Architecture Analysis
Root Layout (layout.tsx)
<div className="flex h-screen bg-gradient-to-br from-background via-background to-muted/20">
<Sidebar />
<main className="flex-1 bg-background/50 backdrop-blur-sm overflow-y-auto">
{children}
</main>
</div>
- The
<Sidebar>is always rendered as a direct flex child. - No conditional rendering based on screen size.
- No shared state mechanism for mobile sidebar open/close.
Sidebar (sidebar.tsx)
- 282 lines, manages its own
isCollapsedstate locally. - Contains: nav links (Home, Videos, Photos, Bookmarks, Surprise Me, Settings), collapsible Clusters section, collapsible Libraries section, and a footer.
- Width toggles between
w-64andw-16via CSS transition. - No
md:orlg:responsive breakpoint classes — purely desktop-oriented.
Content Pages
| Page | Component | Grid Behavior |
|---|---|---|
/videos |
InfiniteVirtualGrid |
2-7 columns based on containerWidth |
/photos |
InfiniteVirtualGrid |
Same as above |
/bookmarks |
InfiniteVirtualGrid |
Same as above |
/folder-viewer |
VirtualizedFolderGrid |
2-7 columns based on containerWidth |
/surprise-me |
CSS grid | grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 |
/settings |
Manual layout | grid-cols-1 lg:grid-cols-3 |
/ (Home) |
Manual layout | grid-cols-1 md:grid-cols-2 lg:grid-cols-4 |
[!NOTE] The virtualized grids (
InfiniteVirtualGrid,VirtualizedFolderGrid) already calculate column count responsively viacontainerWidth. They would benefit automatically from having more width available once the sidebar is hidden on mobile.
Tailwind Configuration (tailwind.config.ts)
- TailwindCSS v3.4.17 (confirmed in
package.json) - Uses
darkMode: ["class"]with.darkclass on<html> - Default breakpoints (
sm: 640px,md: 768px,lg: 1024px,xl: 1280px,2xl: 1536px) - No custom mobile breakpoints defined
Design Decisions
[!NOTE] The following decisions were confirmed by the project owner:
- YouTube-style bottom tab bar for primary mobile navigation (not just a hamburger drawer)
lgbreakpoint (1024px) as the mobile/desktop cutoff — tablets in portrait get the mobile experience- Clusters & Libraries sections collapsed by default in the mobile drawer
Proposed Solution
Approach: Bottom Tab Bar + Hamburger Drawer + Persistent Desktop Sidebar
On desktop (lg and above, ≥1024px): keep the current sidebar as-is (persistent, collapsible).
On mobile/tablet (< lg, <1024px): hide the sidebar, show a YouTube-style bottom tab bar for primary navigation, and provide a hamburger menu in a slim top bar that opens the full sidebar as a slide-in drawer overlay for secondary navigation (Clusters, Libraries, Settings).
This mirrors the YouTube mobile app pattern:
- Bottom tab bar = quick access to primary sections (Home, Videos, Photos, Bookmarks)
- Hamburger drawer = full sidebar with all sections including Clusters, Libraries, Settings
- Top bar = branding + hamburger trigger
Detailed Implementation Plan
Phase 1: Shared Sidebar State (Context)
[!IMPORTANT] The sidebar's open/close state currently lives inside
sidebar.tsxas a localuseState. To control it from the top bar (hamburger button) and coordinate with the bottom tab bar, we need to lift this state into a React Context.
[NEW] src/contexts/sidebar-context.tsx
Create a SidebarProvider context that provides:
interface SidebarContextType {
isCollapsed: boolean; // Desktop collapse state
isMobileOpen: boolean; // Mobile drawer open state
toggleCollapse: () => void; // Desktop toggle
openMobile: () => void; // Open mobile drawer
closeMobile: () => void; // Close mobile drawer
isMobile: boolean; // Current screen is mobile (<1024px)
}
- Use
window.matchMedia('(min-width: 1024px)')to detect mobile vs desktop (usinglgbreakpoint). - Auto-close mobile drawer on route change (listen to
usePathname()). - Auto-close mobile drawer when screen resizes above
lgbreakpoint.
Phase 2: Mobile Top Bar Component
[NEW] src/components/mobile-top-bar.tsx
A slim fixed top bar visible only on < lg screens:
┌──────────────────────────────────────────┐
│ ☰ │ NextAV │ [🔍] │
└──────────────────────────────────────────┘
- Left: Hamburger icon (
Menufrom lucide) → triggersopenMobile()to open the full drawer - Center: "NextAV" branding
- Right: Optional search icon or current section indicator
- CSS:
lg:hidden fixed top-0 left-0 right-0 z-40 h-14 bg-card border-b border-border - Add
pt-14to<main>on mobile to account for the fixed top bar height
[!NOTE] The top bar provides access to the drawer (secondary nav). The bottom tab bar (Phase 2B) handles primary navigation.
Phase 2B: Bottom Tab Bar Component (YouTube-style)
[NEW] src/components/mobile-bottom-tabs.tsx
A fixed bottom tab bar visible only on < lg screens, providing quick access to primary sections:
┌──────────────────────────────────────────┐
│ 🏠 │ 🎬 │ 📷 │ 🔖 │
│ Home │ Videos │ Photos │ Bookmarks │
└──────────────────────────────────────────┘
- 4 primary tabs: Home, Videos, Photos, Bookmarks
- Active tab highlighted with primary color (filled icon + colored label)
- Inactive tabs use muted color
- CSS:
lg:hidden fixed bottom-0 left-0 right-0 z-40 h-16 bg-card border-t border-border backdrop-blur-lg - Uses
safe-area-inset-bottompadding for devices with home indicators (iPhone notch) - Each tab is a
<Link>using Next.js router, with active state derived fromusePathname() - Add
pb-16to<main>on mobile to account for the fixed bottom bar height
Why 4 tabs? YouTube uses 4-5 tabs. The remaining sections (Surprise Me, Settings, Clusters, Libraries) live in the hamburger drawer — they're less frequently accessed.
Tab touch targets: Each tab should be at minimum 48px × 48px (WCAG 2.5.8), with the full tab area being tappable.
Phase 3: Sidebar Refactoring
[MODIFY] src/components/sidebar.tsx
Desktop behavior (lg: and above):
- Keep current behavior: persistent sidebar,
w-64/w-16collapse toggle. - Add
hidden lg:flexto the sidebar container.
Mobile behavior (< lg):
- Render as a fixed overlay drawer that slides in from the left (opened via hamburger in top bar).
- Include a backdrop (
bg-black/50) that closes the drawer on tap. - Use
translate-xanimation for the slide-in/out effect. - The drawer should be
w-72(slightly wider than desktop'sw-64for better touch targets). - Add a close button (
Xicon) in the drawer header. - Nav items should have larger touch targets (
min-h-[48px]per Material Design guidelines). - Clusters and Libraries sections collapsed by default —
showClustersandshowLibrariesshould initialize tofalsewhen in mobile mode.
Structural approach — two render paths in the same component:
// Desktop: persistent sidebar
<aside className="hidden lg:flex flex-col ...">
{/* existing sidebar content */}
</aside>
// Mobile: overlay drawer
{isMobileOpen && (
<div className="lg:hidden fixed inset-0 z-50">
<div className="absolute inset-0 bg-black/50" onClick={closeMobile} />
<aside className="relative w-72 h-full bg-card animate-slide-in ...">
{/* same sidebar content with larger touch targets */}
{/* Clusters: showClusters defaults to false */}
{/* Libraries: showLibraries defaults to false */}
</aside>
</div>
)}
[!TIP] On mobile, the drawer acts as secondary navigation — users who need Clusters, Libraries, Surprise Me, or Settings will open it from the hamburger. The bottom tab bar handles the 4 most common destinations without opening the drawer.
Phase 4: Layout Adjustments
[MODIFY] src/app/layout.tsx
<SidebarProvider>
<div className="flex h-screen">
<MobileTopBar /> {/* visible only < lg */}
<Sidebar /> {/* persistent on lg+, drawer on mobile */}
<main className="flex-1 overflow-y-auto pt-14 lg:pt-0 pb-16 lg:pb-0">
{children}
</main>
<MobileBottomTabs /> {/* visible only < lg */}
</div>
</SidebarProvider>
[!WARNING] Both
pt-14(top bar) andpb-16(bottom tab bar) must be applied on mobile only (lg:pt-0 lg:pb-0) to avoid wasting space on desktop.
[MODIFY] src/app/globals.css
Add animation keyframes for the drawer slide-in:
@keyframes slideInFromLeft {
from { transform: translateX(-100%); }
to { transform: translateX(0); }
}
@keyframes slideOutToLeft {
from { transform: translateX(0); }
to { transform: translateX(-100%); }
}
.animate-slide-in {
animation: slideInFromLeft 0.3s ease-out forwards;
}
.animate-slide-out {
animation: slideOutToLeft 0.3s ease-in forwards;
}
Phase 5: Touch & Mobile UX Enhancements
Touch Targets
- Increase nav button heights from
h-10tomin-h-[48px]on mobile (WCAG 2.5.8). - Increase icon sizes from
h-4 w-4toh-5 w-5on mobile. - Increase font sizes from
text-smtotext-baseon mobile for nav labels.
Swipe Gesture (Optional, Phase 2)
- Add a swipe-right gesture on the left edge of the screen to open the drawer.
- Add a swipe-left gesture on the open drawer to close it.
- Can be implemented with pointer events (
onTouchStart,onTouchMove,onTouchEnd).
Content Adjustments
- The
InfiniteVirtualGridandVirtualizedFolderGridwill automatically gain more width since the sidebar no longer takes space on mobile. - Consider reducing padding from
p-6top-3on mobile for content pages.
Files to Change
| File | Action | Summary |
|---|---|---|
src/contexts/sidebar-context.tsx |
NEW | Shared sidebar state context with mobile detection at lg breakpoint |
src/components/mobile-top-bar.tsx |
NEW | Mobile-only slim top bar with hamburger + branding |
src/components/mobile-bottom-tabs.tsx |
NEW | YouTube-style bottom tab bar (Home, Videos, Photos, Bookmarks) |
src/components/sidebar.tsx |
MODIFY | Dual-mode: persistent desktop (lg:flex) + overlay drawer mobile; Clusters/Libraries collapsed by default on mobile |
src/app/layout.tsx |
MODIFY | Wrap with SidebarProvider, add MobileTopBar + MobileBottomTabs, adjust <main> padding (pt-14 pb-16 lg:pt-0 lg:pb-0) |
src/app/globals.css |
MODIFY | Add slide-in/out animations, safe-area-inset-bottom support |
tailwind.config.ts |
MODIFY (optional) | Add custom animation keyframes if preferred over raw CSS |
Visual Mockup
Desktop (≥1024px) — No Change
┌─────────────┬────────────────────────────────────────┐
│ ▶ NextAV │ │
│─────────────│ Main Content Area │
│ 🏠 Home │ │
│ 🎬 Videos │ ┌──────┐ ┌──────┐ ┌──────┐ │
│ 📷 Photos │ │ Card │ │ Card │ │ Card │ │
│ 🔖 Bookmarks│ └──────┘ └──────┘ └──────┘ │
│ ✨ Surprise │ ┌──────┐ ┌──────┐ ┌──────┐ │
│ ⚙ Settings │ │ Card │ │ Card │ │ Card │ │
│─────────────│ └──────┘ └──────┘ └──────┘ │
│ Clusters │ │
│ Libraries │ │
│─────────────│ │
│ NextAV v1.0│ │
└─────────────┴────────────────────────────────────────┘
Mobile/Tablet (<1024px) — Bottom Tab Bar + Top Bar
┌──────────────────────────┐
│ ☰ │ NextAV │ 🔍 │ ← slim top bar (h-14)
├──────────────────────────┤
│ │
│ ┌──────────┐ ┌────────┐│
│ │ Card │ │ Card ││
│ └──────────┘ └────────┘│
│ ┌──────────┐ ┌────────┐│
│ │ Card │ │ Card ││
│ └──────────┘ └────────┘│
│ ┌──────────┐ ┌────────┐│
│ │ Card │ │ Card ││
│ └──────────┘ └────────┘│
│ │
├──────────────────────────┤
│ 🏠 Home │🎬 Videos│📷 Photos│🔖 Marks│ ← bottom tab bar (h-16)
└──────────────────────────┘
Mobile/Tablet — Hamburger Drawer Open
┌──────────────────────────┐
│ ▶ NextAV ✕ │▒▒▒▒▒│
│────────────────────│▒▒▒▒▒│
│ 🏠 Home │▒▒▒▒▒│
│ 🎬 Videos │▒▒▒▒▒│
│ 📷 Photos │▒▒▒▒▒│
│ 🔖 Bookmarks │▒▒▒▒▒│
│ ✨ Surprise Me │▒▒▒▒▒│
│ ⚙ Settings │▒▒▒▒▒│
│────────────────────│▒▒▒▒▒│
│ ▸ Clusters (collapsed) │
│ ▸ Libraries (collapsed) │
│────────────────────│▒▒▒▒▒│
│ NextAV v1.0 │▒▒▒▒▒│
└──────────────────────────┘
▲ drawer (w-72) ▲ backdrop
Note: Clusters & Libraries are collapsed
by default on mobile. User can expand them.
Implementation Priority
| Priority | Task | Effort |
|---|---|---|
| P0 | Create SidebarProvider context |
Small |
| P0 | Create MobileTopBar component |
Small |
| P0 | Create MobileBottomTabs component (YouTube-style) |
Medium |
| P0 | Refactor sidebar.tsx for desktop/mobile dual mode |
Medium |
| P0 | Update layout.tsx to integrate all pieces |
Small |
| P0 | Add CSS animations for drawer + safe-area support | Small |
| P1 | Increase touch targets on mobile nav items & drawer | Small |
| P1 | Reduce content padding on mobile (p-6 → p-3) |
Small |
| P2 | Swipe gesture to open/close drawer | Medium |
Resolved Decisions
| # | Question | Decision |
|---|---|---|
| 1 | Bottom Tab Bar vs Hamburger Drawer? | YouTube-style bottom tab bar for primary nav (Home, Videos, Photos, Bookmarks). Hamburger drawer retained for secondary nav (Surprise Me, Settings, Clusters, Libraries). |
| 2 | Breakpoint Selection | lg (1024px) — tablets in portrait mode get the mobile experience with bottom tab bar + drawer. |
| 3 | Sidebar Sections on Mobile | Collapsed by default — Clusters and Libraries sections start collapsed in the mobile drawer to reduce scroll length. Users can expand them manually. |