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:
- Extract
Headerchildren (existing) - Extract
Footerchildren - Partition remaining children into
topSectionsandbottomSectionsbased onpositionprop (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— addpositionandmaxHeightprops toSidebarSectionProps, addsectionContentwrapper toSidebarSection, partition children inSidebarRootsrc/design-system/layout/Sidebar/Sidebar.module.css— add.sectionGroup,.sectionSpacer,.sectionContent, custom scrollbar stylessrc/design-system/layout/Sidebar/Sidebar.test.tsx— new test cases for positioning and scrolling behavior
Tests
- Section with
maxHeightrenders content wrapper with correct inline style andoverflow-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