Sidebar: collapse/expand toggle + narrow-viewport auto-hide #162

Closed
opened 2026-06-20 01:55:28 +00:00 by james · 0 comments
Owner

Follow-up to #140 (closed by #161), which deferred collapse behavior:

Mobile / narrow-viewport collapse — follow-up once tablet/phone use matters.

Add a manual collapse/expand toggle to the left sidebar (app/(app)/components/sidebar.tsx) and a narrow-viewport mode where the sidebar hides entirely and only the toggle button remains.

Behavior

Wide viewport, expanded (default):

  • Brand row shows logo + "Carol" wordmark on the left.
  • A small sidebar-toggle icon button sits at the right edge of the brand row (aligned to the sidebar's right border).
  • Clicking it collapses the sidebar.

Wide viewport, collapsed:

  • Sidebar narrows to a slim rail (icons only).
  • The logo + wordmark are replaced by the sidebar-toggle icon button, centered in the brand row's position. Clicking it expands the sidebar again.
  • Nav items show icons only — labels hidden visually but still part of the link's accessible name.
  • Footer (theme switch + user card + logout) collapses to a vertical stack of icon-only buttons:
    • Theme switch becomes a single icon button that cycles or toggles light/dark (no segmented control).
    • User card collapses to just the avatar tile.
    • Logout stays as an icon button.
  • Each collapsed nav item / footer button gets a tooltip on hover/focus revealing its label. Use a native title attribute and a small visual tooltip component so sighted users see the label without waiting for the OS tooltip delay. Screen readers continue to use the link/button's accessible name.

Narrow viewport (below ~900px, exact breakpoint TBD by implementer):

  • The sidebar is hidden entirely — neither the expanded bar nor the collapsed rail is visible by default.
  • Only the sidebar-toggle button remains, floating in the top-left of the viewport (or anchored to a slim header — implementer's call, whichever reads cleanest against the main content's top edge).
  • Tapping the toggle opens the sidebar as an overlay/drawer over the main content (full expanded version — labels visible).
  • Tapping the toggle again, or tapping the scrim outside the drawer, hides it.
  • The narrow-viewport state is not "collapsed" — it's "hidden". The collapsed-rail mode is wide-viewport-only.

Persistence

  • Collapsed/expanded state persists to localStorage (per browser). Key suggestion: carol:sidebar:collapsed"1" | "0".
  • No cookie, no DB column, no server round-trip. (Theme uses the ADR-0008 cookie+DB pattern; we're deliberately picking the lighter option here because the collapse preference is a per-device UX choice, not a per-user identity setting.)
  • On first load with no stored value, default to expanded on wide viewports.
  • On narrow viewports the stored value drives whether the overlay starts open; default closed.

Icon

Use PanelLeft / PanelLeftClose (or Sidebar / SidebarClose) from lucide-react. Pick whichever pair reads most clearly as a "collapse the left rail" affordance. Same icon library already used by sidebar-nav.tsx.

i18n

Add to messages/en.json under the nav namespace:

  • nav.collapseSidebar — "Collapse sidebar" (aria-label + tooltip when expanded)
  • nav.expandSidebar — "Expand sidebar" (aria-label + tooltip when collapsed or hidden)

Per CLAUDE.md, sibling locales fall back to English per-key — they don't need to be updated in the same PR.

Acceptance criteria

  • Wide viewport: a sidebar-toggle icon button sits at the right edge of the brand row when expanded.
  • Clicking the toggle collapses the sidebar to an icons-only rail; the logo+wordmark are replaced by the same toggle button (now centered) which expands it again.
  • When collapsed, nav items show icons only; hovering or focusing one reveals a visual tooltip with the translated label.
  • When collapsed, the footer becomes a vertical stack of icon-only buttons (single theme-toggle icon, avatar tile, logout icon), each with a tooltip.
  • Below ~900px viewport width, the sidebar is hidden entirely; only the toggle button is visible and tapping it reveals the full expanded sidebar as an overlay.
  • Collapsed/expanded preference persists across reloads via localStorage on the same browser.
  • Keyboard: the toggle is reachable via Tab; tooltips appear on focus; nav items remain reachable in both states.
  • aria-current="page" on the active nav item still works in both states.
  • Both themes (light/dark) render correctly in expanded, collapsed, and narrow-viewport modes.
  • Two new nav.* i18n keys added to messages/en.json.

Out of scope

  • Cookie / DB-backed persistence (deliberately localStorage-only — see Persistence above).
  • A separate hamburger button elsewhere in the layout — the only toggle is the sidebar-toggle icon itself.
  • Touch-gesture swipe-to-open on the narrow-viewport drawer.
  • Animating between states (a simple CSS transition on width is fine; no spring physics).

Composes with

  • #140 (sidebar app shell; closed by #161).
  • ADR-0023 (Carol DS tokens — keep using the --surface, --text, --border, --accent aliases already in sidebar.module.css).
  • ADR-0025 (next-intl — add new strings to messages/en.json nav namespace).
Follow-up to #140 (closed by #161), which deferred collapse behavior: > Mobile / narrow-viewport collapse — follow-up once tablet/phone use matters. Add a manual collapse/expand toggle to the left sidebar (`app/(app)/components/sidebar.tsx`) and a narrow-viewport mode where the sidebar hides entirely and only the toggle button remains. ## Behavior **Wide viewport, expanded (default):** - Brand row shows logo + "Carol" wordmark on the left. - A small sidebar-toggle icon button sits at the **right edge** of the brand row (aligned to the sidebar's right border). - Clicking it collapses the sidebar. **Wide viewport, collapsed:** - Sidebar narrows to a slim rail (icons only). - The logo + wordmark are **replaced by the sidebar-toggle icon button**, centered in the brand row's position. Clicking it expands the sidebar again. - Nav items show **icons only** — labels hidden visually but still part of the link's accessible name. - Footer (theme switch + user card + logout) collapses to a vertical stack of icon-only buttons: - Theme switch becomes a single icon button that cycles or toggles light/dark (no segmented control). - User card collapses to just the avatar tile. - Logout stays as an icon button. - Each collapsed nav item / footer button gets a tooltip on hover/focus revealing its label. Use a native `title` attribute **and** a small visual tooltip component so sighted users see the label without waiting for the OS tooltip delay. Screen readers continue to use the link/button's accessible name. **Narrow viewport (below ~900px, exact breakpoint TBD by implementer):** - The sidebar is **hidden entirely** — neither the expanded bar nor the collapsed rail is visible by default. - Only the sidebar-toggle button remains, floating in the top-left of the viewport (or anchored to a slim header — implementer's call, whichever reads cleanest against the main content's top edge). - Tapping the toggle opens the sidebar as an overlay/drawer over the main content (full expanded version — labels visible). - Tapping the toggle again, or tapping the scrim outside the drawer, hides it. - The narrow-viewport state is not "collapsed" — it's "hidden". The collapsed-rail mode is wide-viewport-only. ## Persistence - Collapsed/expanded state persists to **localStorage** (per browser). Key suggestion: `carol:sidebar:collapsed` → `"1" | "0"`. - No cookie, no DB column, no server round-trip. (Theme uses the ADR-0008 cookie+DB pattern; we're deliberately picking the lighter option here because the collapse preference is a per-device UX choice, not a per-user identity setting.) - On first load with no stored value, default to **expanded** on wide viewports. - On narrow viewports the stored value drives whether the overlay starts open; default closed. ## Icon Use `PanelLeft` / `PanelLeftClose` (or `Sidebar` / `SidebarClose`) from `lucide-react`. Pick whichever pair reads most clearly as a "collapse the left rail" affordance. Same icon library already used by `sidebar-nav.tsx`. ## i18n Add to `messages/en.json` under the `nav` namespace: - `nav.collapseSidebar` — "Collapse sidebar" (aria-label + tooltip when expanded) - `nav.expandSidebar` — "Expand sidebar" (aria-label + tooltip when collapsed or hidden) Per CLAUDE.md, sibling locales fall back to English per-key — they don't need to be updated in the same PR. ## Acceptance criteria - [ ] Wide viewport: a sidebar-toggle icon button sits at the right edge of the brand row when expanded. - [ ] Clicking the toggle collapses the sidebar to an icons-only rail; the logo+wordmark are replaced by the same toggle button (now centered) which expands it again. - [ ] When collapsed, nav items show icons only; hovering or focusing one reveals a visual tooltip with the translated label. - [ ] When collapsed, the footer becomes a vertical stack of icon-only buttons (single theme-toggle icon, avatar tile, logout icon), each with a tooltip. - [ ] Below ~900px viewport width, the sidebar is hidden entirely; only the toggle button is visible and tapping it reveals the full expanded sidebar as an overlay. - [ ] Collapsed/expanded preference persists across reloads via `localStorage` on the same browser. - [ ] Keyboard: the toggle is reachable via Tab; tooltips appear on focus; nav items remain reachable in both states. - [ ] `aria-current="page"` on the active nav item still works in both states. - [ ] Both themes (light/dark) render correctly in expanded, collapsed, and narrow-viewport modes. - [ ] Two new `nav.*` i18n keys added to `messages/en.json`. ## Out of scope - Cookie / DB-backed persistence (deliberately localStorage-only — see Persistence above). - A separate hamburger button elsewhere in the layout — the only toggle is the sidebar-toggle icon itself. - Touch-gesture swipe-to-open on the narrow-viewport drawer. - Animating between states (a simple CSS `transition` on width is fine; no spring physics). ## Composes with - #140 (sidebar app shell; closed by #161). - ADR-0023 (Carol DS tokens — keep using the `--surface`, `--text`, `--border`, `--accent` aliases already in `sidebar.module.css`). - ADR-0025 (next-intl — add new strings to `messages/en.json` `nav` namespace).
james closed this issue 2026-06-20 02:37:17 +00:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
james/carol#162
No description provided.