Files
design-system/docs/superpowers/specs/2026-04-15-sidebar-section-layout-design.md
hsiegeln 1c2c00d266 docs: add sidebar section layout design spec
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 20:51:12 +02:00

5.4 KiB

Sidebar Section Layout: Top/Bottom Positioning & Scrollable Sections

Summary

Extend Sidebar.Section with two new optional props:

  • position: 'top' | 'bottom' — controls whether a section stacks from the top of the sidebar or from the bottom (above the footer). Default: 'top'.
  • maxHeight: string — CSS length value (e.g. "250px", "30vh") that constrains the section's content area. When content exceeds this height, it scrolls. The section header/toggle always remains visible.

This enables a layout where primary sections (Applications, Agents) occupy the top of the sidebar, while secondary sections (Routes, Starred) cluster near the bottom — with a flexible spacer absorbing remaining vertical space between the two groups.

API Changes

Sidebar.Section — new optional props

interface SidebarSectionProps {
  icon: ReactNode
  label: string
  open: boolean
  onToggle: () => void
  active?: boolean
  children: ReactNode
  className?: string
  position?: 'top' | 'bottom'  // default: 'top'
  maxHeight?: string            // CSS length, e.g. "250px", "30vh"
}

No changes to Sidebar.Header, Sidebar.Footer, Sidebar.FooterLink, or SidebarRoot props.

Consumer usage

<Sidebar>
  <Sidebar.Header ... />

  <Sidebar.Section label="Applications" ...>
    <SidebarTree ... />
  </Sidebar.Section>

  <Sidebar.Section label="Agents" ...>
    <SidebarTree ... />
  </Sidebar.Section>

  <Sidebar.Section label="Routes" position="bottom" maxHeight="200px" ...>
    <SidebarTree ... />
  </Sidebar.Section>

  <Sidebar.Section label="Starred" position="bottom" ...>
    ...
  </Sidebar.Section>

  <Sidebar.Footer>...</Sidebar.Footer>
</Sidebar>

Layout Model

SidebarRoot child partitioning

SidebarRoot already inspects children to extract Header. This extends the same pattern:

  1. Extract Header children (existing)
  2. Extract Footer children
  3. Partition remaining children into topSections and bottomSections based on position prop (default 'top')

Render structure

<aside class="sidebar">
  [collapse toggle]
  {header}
  {search bar}
  <div class="sectionGroup sectionGroupTop">
    {topSections}
  </div>
  <div class="sectionSpacer" />
  <div class="sectionGroup sectionGroupBottom">
    {bottomSections}
  </div>
  {footer}
</aside>

When there are no bottom sections, the spacer is omitted. The layout behaves identically to today — footer's margin-top: auto handles positioning. Zero breaking changes for existing consumers.

SidebarSection content wrapper

A new .sectionContent div wraps only the children inside SidebarSection (not the toggle header). When maxHeight is provided, it receives the value as an inline max-height style.

{open && (
  <div
    className={styles.sectionContent}
    style={maxHeight ? { maxHeight } : undefined}
  >
    {children}
  </div>
)}

CSS Changes

Group wrappers

.sectionGroup {
  flex: 0 1 auto;
  overflow-y: auto;
  min-height: 0;
}

.sectionSpacer {
  flex: 1 0 0;
}

Section content scrolling

.sectionContent {
  overflow-y: auto;
}

maxHeight is applied as an inline style, not a CSS class, since it varies per section instance.

Custom scrollbars

Applied to both .sectionGroup and .sectionContent to keep the dark sidebar aesthetic:

/* Standard (Firefox, modern Chrome/Edge) */
scrollbar-width: thin;
scrollbar-color: rgba(255, 255, 255, 0.15) transparent;

/* WebKit (Safari, older Chrome) */
::-webkit-scrollbar {
  width: 4px;
}

::-webkit-scrollbar-track {
  background: transparent;
}

::-webkit-scrollbar-thumb {
  background: rgba(255, 255, 255, 0.15);
  border-radius: 2px;
}

::-webkit-scrollbar-thumb:hover {
  background: rgba(255, 255, 255, 0.3);
}

Collapsed Sidebar Behavior

In collapsed (icon rail) mode, sections render as single icon buttons with no scrollable content:

  • position: Respected — bottom sections render in the bottom group, so their icons cluster near the footer in the rail.
  • maxHeight: Ignored — no content to constrain.

The group wrapper and spacer structure remains active in collapsed mode for consistent icon positioning.

Edge Cases

Scenario Behavior
All sections "top" (no bottom sections) No spacer rendered. Identical to current layout.
All sections "bottom" Top group empty. Sections cluster above footer.
maxHeight set but content is shorter No visual effect — wrapper is naturally smaller than max.
Very short viewport Both group wrappers scroll independently via overflow-y: auto on .sectionGroup.

Files Changed

  • src/design-system/layout/Sidebar/Sidebar.tsx — add position and maxHeight props to SidebarSectionProps, add sectionContent wrapper to SidebarSection, partition children in SidebarRoot
  • src/design-system/layout/Sidebar/Sidebar.module.css — add .sectionGroup, .sectionSpacer, .sectionContent, custom scrollbar styles
  • src/design-system/layout/Sidebar/Sidebar.test.tsx — new test cases for positioning and scrolling behavior

Tests

  • Section with maxHeight renders content wrapper with correct inline style and overflow-y: auto
  • Sections with position="bottom" render inside the bottom group wrapper
  • Default position (omitted) renders in the top group
  • When no bottom sections exist, no spacer is rendered
  • Collapsed sidebar renders bottom sections in the bottom group