An interactive demonstration of collapsible vertical sidebar concepts. Compare rail expanding, search-centric list, split grid, and multi-tier lists.
Sidebar Navigation Laboratory
Compare different vertical sidebar styles including accordion menus, icon rails with hover drawers, search-first links, split lists, or quick card drawers.
Main content area
Main Application Area (Simulated)
Variant1_AccordionSections.tsx (Widget Implementation)
import { useState } from "react";
import { Text, Group, Box, UnstyledButton, Badge, ScrollArea, Collapse } from "@mantine/core";
import { HiChevronRight, HiChevronDown } from "react-icons/hi2";
import { SidebarShell } from "./SidebarShell";
import type { SidebarNavProps, SidebarLink, SidebarSection } from "./types";
const SKY = "#4fc3f7";
function LinkItem({
link,
onClick,
indent = 0,
}: {
link: SidebarLink;
onClick: (link: SidebarLink) => void;
indent?: number;
}) {
return (
<UnstyledButton
onClick={() => onClick(link)}
w="100%"
px="md"
py={5}
pl={16 + indent}
style={{
borderRadius: 4,
transition: "background-color 150ms ease",
}}
onMouseOver={(e) => {
e.currentTarget.style.backgroundColor = "#e3f2fd";
}}
onMouseOut={(e) => {
e.currentTarget.style.backgroundColor = "transparent";
}}
>
<Group gap="xs" justify="space-between" wrap="nowrap">
<Group gap="xs" wrap="nowrap">
<Box c="gray.5" style={{ flexShrink: 0 }}>
{link.icon}
</Box>
<Text size="sm" lineClamp={1}>
{link.label}
</Text>
</Group>
{link.badge && (
<Badge size="xs" variant="light" color="orange" style={{ flexShrink: 0 }}>
{link.badge}
</Badge>
)}
</Group>
</UnstyledButton>
);
}
function SectionAccordion({
section,
isOpen,
onToggle,
onLinkClick,
}: {
section: SidebarSection;
isOpen: boolean;
onToggle: () => void;
onLinkClick: (link: SidebarLink) => void;
}) {
const [openSubs, setOpenSubs] = useState<Set<string>>(new Set());
const toggleSub = (id: string) => {
setOpenSubs((prev) => {
const next = new Set(prev);
if (next.has(id)) next.delete(id);
else next.add(id);
return next;
});
};
const totalLinks =
section.links.length + (section.subSections?.reduce((sum, s) => sum + s.links.length, 0) ?? 0);
return (
<div>
<UnstyledButton
onClick={onToggle}
w="100%"
px="md"
py="sm"
style={{
backgroundColor: isOpen ? "#e3f2fd" : "transparent",
transition: "background-color 150ms ease",
}}
onMouseOver={(e) => {
if (!isOpen) e.currentTarget.style.backgroundColor = "#f1f3f5";
}}
onMouseOut={(e) => {
if (!isOpen) e.currentTarget.style.backgroundColor = "transparent";
}}
>
<Group gap="xs" justify="space-between" wrap="nowrap">
<Group gap="xs" wrap="nowrap">
<Box c={isOpen ? SKY : "gray.5"} style={{ flexShrink: 0 }}>
{section.icon}
</Box>
<Text size="sm" fw={600} c={isOpen ? SKY : undefined} lineClamp={1}>
{section.label}
</Text>
</Group>
<Group gap={6} wrap="nowrap" style={{ flexShrink: 0 }}>
{section.badge && (
<Badge size="xs" variant="light" color="blue">
{section.badge}
</Badge>
)}
<Text size="xs" c="dimmed">
{totalLinks}
</Text>
{isOpen ? (
<HiChevronDown size={14} color="#adb5bd" />
) : (
<HiChevronRight size={14} color="#adb5bd" />
)}
</Group>
</Group>
</UnstyledButton>
<Collapse expanded={isOpen}>
<div style={{ paddingBottom: 4 }}>
{/* Direct links */}
{section.links.map((link) => (
<LinkItem key={link.id} link={link} onClick={onLinkClick} indent={16} />
))}
{/* Sub-sections as nested accordions */}
{section.subSections?.map((sub) => (
<div key={sub.id}>
<UnstyledButton
onClick={() => toggleSub(sub.id)}
w="100%"
px="md"
py={6}
pl={32}
style={{
transition: "background-color 150ms ease",
}}
onMouseOver={(e) => {
e.currentTarget.style.backgroundColor = "#f1f3f5";
}}
onMouseOut={(e) => {
e.currentTarget.style.backgroundColor = "transparent";
}}
>
<Group gap="xs" justify="space-between" wrap="nowrap">
<Text size="xs" fw={700} c="dimmed" tt="uppercase">
{sub.label}
</Text>
{openSubs.has(sub.id) ? (
<HiChevronDown size={12} color="#adb5bd" />
) : (
<HiChevronRight size={12} color="#adb5bd" />
)}
</Group>
</UnstyledButton>
<Collapse expanded={openSubs.has(sub.id)}>
{sub.links.map((link) => (
<LinkItem key={link.id} link={link} onClick={onLinkClick} indent={32} />
))}
</Collapse>
</div>
))}
</div>
</Collapse>
</div>
);
}
export function Variant1_AccordionSections({ sections, onLinkClick }: SidebarNavProps) {
const [openSection, setOpenSection] = useState<string | null>(null);
const handleClick = onLinkClick ?? (() => {});
return (
<SidebarShell
sidebar={
<ScrollArea style={{ flex: 1 }}>
<Box py="xs">
{sections.map((section) => (
<SectionAccordion
key={section.id}
section={section}
isOpen={openSection === section.id}
onToggle={() => setOpenSection((prev) => (prev === section.id ? null : section.id))}
onLinkClick={handleClick}
/>
))}
</Box>
</ScrollArea>
}
/>
);
}