Sidebar
This library is a work-in-progress. We are releasing it early to gather feedback, but it is not ready for production.
Experimental Component
The Sidebar is an experimental component that departs from AgnosticUI's typical "primitives only" philosophy. Due to its complex nature and shadow DOM encapsulation, it requires some nuanced global CSS to function properly across all scenarios.
Use at your own risk. If you're comfortable with these architectural tradeoffs, feel free to use the component. We welcome contributions to help improve its implementation and reduce external dependencies.
A collapsible sidebar component for navigation with support for expanded/collapsed states, nested navigation, submenus, and responsive behavior. The Sidebar adapts seamlessly across desktop and mobile viewports with optional rail mode (icon-only) on desktop and overlay mode on mobile.
Examples
Live Preview
Default Sidebar with Navigation
Demonstrates the sidebar with navigation items, submenus, and both expanded/collapsed states. Click the header toggle to collapse into rail mode (icon-only).
Dashboard
Main Content
Click the header toggle to collapse the sidebar into rail mode.
When collapsed, hover over items with submenus to see them in popovers.
Sidebar with Header Actions
Demonstrates using ag-header-start, ag-header-end, and ag-header-toggle slots for a composable header layout with action buttons.
My Application
Header Actions Pattern
The header includes a settings button in the ag-header-end slot.
This allows for flexible header layouts with multiple action buttons.
Sidebar with Built-in Toggle
Using show-header-toggle adds a built-in collapse button automatically.
Built-in Toggle
Built-in Header Toggle
No need to provide a custom toggle button—the sidebar includes one automatically.
Disable Compact Mode
With disable-compact-mode, the sidebar has no intermediate collapsed/rail state. It's either fully open (expanded) or completely hidden. This pattern is used in applications like Claude AI Studio.
AI Studio
Disable Compact Mode
Use the mobile toggle button to show/hide the sidebar.
Notice there's no collapsed/rail mode—it's either fully visible or completely hidden.
Active Item Tracking
Click navigation items to see the active state change. This demonstrates how to track the current route and apply active styling to both top-level and submenu items.
Navigation
Active Item Tracking
Current route: /dashboard
Click navigation items to see the active state change.
- Active styling: Background color and font weight change
- ARIA current: The active item has aria-current="page" for accessibility
- Submenu support: Sublinks also track active state
- Popover sync: Active state works in both inline and popover modes
In a real application, you'd integrate this with your router (Vue Router, etc.) to automatically update the active state based on the current route.
View Vue Code
<template>
<section>
<div class="mbe4">
<h2>Default Sidebar with Navigation</h2>
<p>
Demonstrates the sidebar with navigation items, submenus, and both expanded/collapsed states.
Click the header toggle to collapse into rail mode (icon-only).
</p>
</div>
<div class="mbe6">
<div style="position: relative; display: flex; height: 500px; border: 1px solid var(--ag-border-color); border-radius: 0.5rem; overflow: hidden; contain: layout;">
<VueSidebar
:open="sidebar1.isOpen"
:collapsed="sidebar1.isCollapsed"
:show-mobile-toggle="true"
@update:open="sidebar1.isOpen = $event"
@update:collapsed="sidebar1.isCollapsed = $event"
>
<h2
slot="ag-header-start"
style="margin: 0; font-size: 1.125rem; font-weight: 600;"
>
Dashboard
</h2>
<button
type="button"
slot="ag-header-toggle"
@click="toggleSidebar1Collapse"
style="background: none; border: none; padding: 8px 0; cursor: pointer; display: flex; align-items: center; color: inherit;"
aria-label="Toggle sidebar"
>
<svg
width="20"
height="20"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<rect
width="18"
height="18"
x="3"
y="3"
rx="2"
/>
<path d="M9 3v18" />
</svg>
</button>
<VueSidebarNav>
<VueSidebarNavItem>
<button
type="button"
class="nav-button active"
aria-current="page"
>
<Home :size="20" />
<span class="nav-label">Dashboard</span>
</button>
</VueSidebarNavItem>
<VueSidebarNavItem>
<button
type="button"
class="nav-button nav-button-expanded"
aria-expanded="false"
@click="handleSubmenuToggle"
>
<Folder :size="20" />
<span class="nav-label">Projects</span>
<span class="chevron">
<ChevronRight :size="16" />
</span>
</button>
<!-- Popover for COLLAPSED mode -->
<VuePopover
class="nav-button-collapsed"
placement="right-start"
trigger-type="click"
:distance="8"
:arrow="true"
.showHeader="false"
>
<button
slot="trigger"
type="button"
class="nav-button"
>
<Folder :size="20" />
<span class="nav-label">Projects</span>
<span class="collapsed-indicator">
<svg
viewBox="0 0 8 8"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M2 3l2 2 2-2"
stroke="currentColor"
stroke-width="1"
stroke-linecap="round"
/>
</svg>
</span>
</button>
<VueSidebarNavPopoverSubmenu
slot="content"
class="popover-submenu"
>
<a
href="#"
class="nav-sublink"
@click.prevent
>Project Alpha</a>
<a
href="#"
class="nav-sublink"
@click.prevent
>Project Beta</a>
<a
href="#"
class="nav-sublink"
@click.prevent
>Project Gamma</a>
</VueSidebarNavPopoverSubmenu>
</VuePopover>
<!-- Inline submenu for expanded mode -->
<VueSidebarNavSubmenu>
<a
class="nav-sublink"
href="#"
@click.prevent
>Project Alpha</a>
<a
class="nav-sublink"
href="#"
@click.prevent
>Project Beta</a>
<a
class="nav-sublink"
href="#"
@click.prevent
>Project Gamma</a>
</VueSidebarNavSubmenu>
</VueSidebarNavItem>
<VueSidebarNavItem>
<button
type="button"
class="nav-button"
>
<User :size="20" />
<span class="nav-label">Team</span>
</button>
</VueSidebarNavItem>
<VueSidebarNavItem>
<button
type="button"
class="nav-button nav-button-expanded"
aria-expanded="false"
@click="handleSubmenuToggle"
>
<Settings :size="20" />
<span class="nav-label">Settings</span>
<span class="chevron">
<ChevronRight :size="16" />
</span>
</button>
<!-- Popover for COLLAPSED mode -->
<VuePopover
class="nav-button-collapsed"
placement="right-start"
trigger-type="click"
:distance="8"
:arrow="true"
.showHeader="false"
>
<button
slot="trigger"
type="button"
class="nav-button"
>
<Settings :size="20" />
<span class="nav-label">Settings</span>
<span class="collapsed-indicator">
<svg
viewBox="0 0 8 8"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M2 3l2 2 2-2"
stroke="currentColor"
stroke-width="1"
stroke-linecap="round"
/>
</svg>
</span>
</button>
<VueSidebarNavPopoverSubmenu
slot="content"
class="popover-submenu"
>
<a
href="#"
class="nav-sublink"
@click.prevent
>Profile</a>
<a
href="#"
class="nav-sublink"
@click.prevent
>Billing</a>
<a
href="#"
class="nav-sublink"
@click.prevent
>Security</a>
<a
href="#"
class="nav-sublink"
@click.prevent
>Preferences</a>
</VueSidebarNavPopoverSubmenu>
</VuePopover>
<VueSidebarNavSubmenu>
<a
class="nav-sublink"
href="#"
@click.prevent
>Profile</a>
<a
class="nav-sublink"
href="#"
@click.prevent
>Billing</a>
<a
class="nav-sublink"
href="#"
@click.prevent
>Security</a>
<a
class="nav-sublink"
href="#"
@click.prevent
>Preferences</a>
</VueSidebarNavSubmenu>
</VueSidebarNavItem>
</VueSidebarNav>
<div
slot="ag-footer"
style="font-size: 0.875rem; color: var(--ag-text-secondary);"
>
© 2024 Company
</div>
</VueSidebar>
<main style="flex: 1; padding: 2rem; overflow: auto; background: var(--ag-background);">
<h1 style="margin-top: 0;">Main Content</h1>
<p>Click the header toggle to collapse the sidebar into rail mode.</p>
<p>When collapsed, hover over items with submenus to see them in popovers.</p>
</main>
</div>
</div>
<div class="mbe4">
<h2>Sidebar with Header Actions</h2>
<p>
Demonstrates using <code>ag-header-start</code>, <code>ag-header-end</code>, and <code>ag-header-toggle</code> slots
for a composable header layout with action buttons.
</p>
</div>
<div class="mbe6">
<div style="position: relative; display: flex; height: 500px; border: 1px solid var(--ag-border-color); border-radius: 0.5rem; overflow: hidden; contain: layout;">
<VueSidebar
:open="sidebar2.isOpen"
:collapsed="sidebar2.isCollapsed"
:show-mobile-toggle="true"
@update:open="sidebar2.isOpen = $event"
@update:collapsed="sidebar2.isCollapsed = $event"
>
<h2
slot="ag-header-start"
style="margin: 0; font-size: 1.125rem; font-weight: 600;"
>
My Application
</h2>
<button
type="button"
slot="ag-header-end"
@click="() => {}"
style="background: none; border: none; padding: 8px; cursor: pointer; display: flex; align-items: center; color: inherit; border-radius: 0.25rem;"
aria-label="Settings"
title="Settings"
>
<Settings :size="20" />
</button>
<button
type="button"
slot="ag-header-toggle"
@click="toggleSidebar2Collapse"
style="background: none; border: none; padding: 8px 0; cursor: pointer; display: flex; align-items: center; color: inherit;"
aria-label="Toggle sidebar"
>
<svg
width="20"
height="20"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<rect
width="18"
height="18"
x="3"
y="3"
rx="2"
/>
<path d="M9 3v18" />
</svg>
</button>
<VueSidebarNav>
<VueSidebarNavItem>
<button
type="button"
class="nav-button active"
aria-current="page"
>
<Home :size="20" />
<span class="nav-label">Dashboard</span>
</button>
</VueSidebarNavItem>
<VueSidebarNavItem>
<button
type="button"
class="nav-button"
>
<Folder :size="20" />
<span class="nav-label">Projects</span>
</button>
</VueSidebarNavItem>
<VueSidebarNavItem>
<button
type="button"
class="nav-button"
>
<User :size="20" />
<span class="nav-label">Team</span>
</button>
</VueSidebarNavItem>
</VueSidebarNav>
<div
slot="ag-footer"
style="font-size: 0.875rem; color: var(--ag-text-secondary);"
>
© 2024 Company
</div>
</VueSidebar>
<main style="flex: 1; padding: 2rem; overflow: auto; background: var(--ag-background);">
<h1 style="margin-top: 0;">Header Actions Pattern</h1>
<p>The header includes a settings button in the <code>ag-header-end</code> slot.</p>
<p>This allows for flexible header layouts with multiple action buttons.</p>
</main>
</div>
</div>
<div class="mbe4">
<h2>Sidebar with Built-in Toggle</h2>
<p>
Using <code>show-header-toggle</code> adds a built-in collapse button automatically.
</p>
</div>
<div class="mbe6">
<div style="position: relative; display: flex; height: 500px; border: 1px solid var(--ag-border-color); border-radius: 0.5rem; overflow: hidden; contain: layout;">
<VueSidebar
:open="sidebar3.isOpen"
:collapsed="sidebar3.isCollapsed"
:show-header-toggle="true"
:show-mobile-toggle="true"
@update:open="sidebar3.isOpen = $event"
@update:collapsed="sidebar3.isCollapsed = $event"
>
<h2
slot="ag-header-start"
style="margin: 0; font-size: 1.125rem; font-weight: 600;"
>
Built-in Toggle
</h2>
<VueSidebarNav>
<VueSidebarNavItem>
<button
type="button"
class="nav-button active"
aria-current="page"
>
<Home :size="20" />
<span class="nav-label">Dashboard</span>
</button>
</VueSidebarNavItem>
<VueSidebarNavItem>
<button
type="button"
class="nav-button"
>
<Folder :size="20" />
<span class="nav-label">Projects</span>
</button>
</VueSidebarNavItem>
<VueSidebarNavItem>
<button
type="button"
class="nav-button"
>
<User :size="20" />
<span class="nav-label">Team</span>
</button>
</VueSidebarNavItem>
</VueSidebarNav>
<div
slot="ag-footer"
style="font-size: 0.875rem; color: var(--ag-text-secondary);"
>
© 2024 Company
</div>
</VueSidebar>
<main style="flex: 1; padding: 2rem; overflow: auto; background: var(--ag-background);">
<h1 style="margin-top: 0;">Built-in Header Toggle</h1>
<p>No need to provide a custom toggle button—the sidebar includes one automatically.</p>
</main>
</div>
</div>
<div class="mbe4">
<h2>Disable Compact Mode</h2>
<p>
With <code>disable-compact-mode</code>, the sidebar has no intermediate collapsed/rail state.
It's either fully open (expanded) or completely hidden. This pattern is used in applications like Claude AI Studio.
</p>
</div>
<div class="mbe6">
<div style="position: relative; display: flex; height: 500px; border: 1px solid var(--ag-border-color); border-radius: 0.5rem; overflow: hidden; contain: layout;">
<VueSidebar
:open="sidebar4.isOpen"
:disable-compact-mode="true"
:show-mobile-toggle="true"
@update:open="sidebar4.isOpen = $event"
>
<h2
slot="ag-header-start"
style="margin: 0; font-size: 1.125rem; font-weight: 600;"
>
AI Studio
</h2>
<button
type="button"
slot="ag-header-toggle"
@click="toggleSidebar4Responsive"
style="background: none; border: none; padding: 8px 0; cursor: pointer; display: flex; align-items: center; color: inherit;"
aria-label="Toggle sidebar"
>
<Command :size="20" />
</button>
<VueSidebarNav>
<VueSidebarNavItem>
<button
type="button"
class="nav-button active"
aria-current="page"
>
<Home :size="20" />
<span class="nav-label">Dashboard</span>
</button>
</VueSidebarNavItem>
<VueSidebarNavItem>
<button
type="button"
class="nav-button"
>
<Folder :size="20" />
<span class="nav-label">Projects</span>
</button>
</VueSidebarNavItem>
<VueSidebarNavItem>
<button
type="button"
class="nav-button"
>
<User :size="20" />
<span class="nav-label">Team</span>
</button>
</VueSidebarNavItem>
<VueSidebarNavItem>
<button
type="button"
class="nav-button"
>
<Settings :size="20" />
<span class="nav-label">Settings</span>
</button>
</VueSidebarNavItem>
</VueSidebarNav>
<div
slot="ag-footer"
style="font-size: 0.875rem; color: var(--ag-text-secondary);"
>
© 2024 Company
</div>
</VueSidebar>
<main style="flex: 1; padding: 2rem; overflow: auto; background: var(--ag-background);">
<h1 style="margin-top: 0;">Disable Compact Mode</h1>
<p>Use the mobile toggle button to show/hide the sidebar.</p>
<p>Notice there's no collapsed/rail mode—it's either fully visible or completely hidden.</p>
</main>
</div>
</div>
<div class="mbe4">
<h2>Active Item Tracking</h2>
<p>
Click navigation items to see the active state change. This demonstrates how to track the current route
and apply active styling to both top-level and submenu items.
</p>
</div>
<div class="mbe6">
<div style="position: relative; display: flex; height: 500px; border: 1px solid var(--ag-border-color); border-radius: 0.5rem; overflow: hidden; contain: layout;">
<VueSidebar
:open="sidebar5.isOpen"
:collapsed="sidebar5.isCollapsed"
:show-mobile-toggle="true"
@update:open="sidebar5.isOpen = $event"
@update:collapsed="sidebar5.isCollapsed = $event"
>
<h2
slot="ag-header-start"
style="margin: 0; font-size: 1.125rem; font-weight: 600;"
>
Navigation
</h2>
<button
type="button"
slot="ag-header-toggle"
@click="toggleSidebar5Collapse"
style="background: none; border: none; padding: 8px 0; cursor: pointer; display: flex; align-items: center; color: inherit;"
aria-label="Toggle sidebar"
>
<svg
width="20"
height="20"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<rect
width="18"
height="18"
x="3"
y="3"
rx="2"
/>
<path d="M9 3v18" />
</svg>
</button>
<VueSidebarNav>
<VueSidebarNavItem>
<button
type="button"
class="nav-button active"
aria-current="page"
data-route="/dashboard"
@click="handleNavClick('/dashboard', $event)"
>
<Home :size="20" />
<span class="nav-label">Dashboard</span>
</button>
</VueSidebarNavItem>
<VueSidebarNavItem>
<button
type="button"
class="nav-button"
data-route="/projects"
@click="handleNavClick('/projects', $event)"
>
<Folder :size="20" />
<span class="nav-label">Projects</span>
</button>
</VueSidebarNavItem>
<VueSidebarNavItem>
<button
type="button"
class="nav-button"
data-route="/team"
@click="handleNavClick('/team', $event)"
>
<User :size="20" />
<span class="nav-label">Team</span>
</button>
</VueSidebarNavItem>
<VueSidebarNavItem>
<button
type="button"
class="nav-button nav-button-expanded"
aria-expanded="false"
data-route="/settings"
@click="handleSettingsClick"
>
<Settings :size="20" />
<span class="nav-label">Settings</span>
<span class="chevron">
<ChevronRight :size="16" />
</span>
</button>
<VuePopover
class="nav-button-collapsed"
placement="right-start"
trigger-type="click"
:distance="8"
:arrow="true"
.showHeader="false"
>
<button
slot="trigger"
type="button"
class="nav-button"
data-route="/settings"
@click="handleNavClick('/settings', $event)"
>
<Settings :size="20" />
<span class="nav-label">Settings</span>
<span class="collapsed-indicator">
<svg
viewBox="0 0 8 8"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M2 3l2 2 2-2"
stroke="currentColor"
stroke-width="1"
stroke-linecap="round"
/>
</svg>
</span>
</button>
<VueSidebarNavPopoverSubmenu
slot="content"
class="popover-submenu"
>
<a
href="#"
class="nav-sublink"
data-route="/settings/profile"
@click="handleNavClick('/settings/profile', $event)"
>Profile</a>
<a
href="#"
class="nav-sublink"
data-route="/settings/billing"
@click="handleNavClick('/settings/billing', $event)"
>Billing</a>
<a
href="#"
class="nav-sublink"
data-route="/settings/security"
@click="handleNavClick('/settings/security', $event)"
>Security</a>
</VueSidebarNavPopoverSubmenu>
</VuePopover>
<VueSidebarNavSubmenu>
<a
class="nav-sublink"
href="#"
data-route="/settings/profile"
@click="handleNavClick('/settings/profile', $event)"
>Profile</a>
<a
class="nav-sublink"
href="#"
data-route="/settings/billing"
@click="handleNavClick('/settings/billing', $event)"
>Billing</a>
<a
class="nav-sublink"
href="#"
data-route="/settings/security"
@click="handleNavClick('/settings/security', $event)"
>Security</a>
</VueSidebarNavSubmenu>
</VueSidebarNavItem>
</VueSidebarNav>
</VueSidebar>
<main style="flex: 1; padding: 2rem; overflow: auto; background: var(--ag-background);">
<h1 style="margin-top: 0;">Active Item Tracking</h1>
<p>Current route: <strong>{{ sidebar5.activeRoute }}</strong></p>
<p>Click navigation items to see the active state change.</p>
<ul>
<li><strong>Active styling:</strong> Background color and font weight change</li>
<li><strong>ARIA current:</strong> The active item has aria-current="page" for accessibility</li>
<li><strong>Submenu support:</strong> Sublinks also track active state</li>
<li><strong>Popover sync:</strong> Active state works in both inline and popover modes</li>
</ul>
<p>
In a real application, you'd integrate this with your router (Vue Router, etc.) to automatically
update the active state based on the current route.
</p>
</main>
</div>
</div>
</section>
</template>
<script>
import VueSidebar from "agnosticui-core/sidebar/vue";
import {
VueSidebarNav,
VueSidebarNavItem,
VueSidebarNavSubmenu,
VueSidebarNavPopoverSubmenu,
} from "agnosticui-core/sidebar-nav/vue";
import { VuePopover } from "agnosticui-core/popover/vue";
import {
Home,
Folder,
User,
Settings,
ChevronRight,
Command,
} from "lucide-vue-next";
export default {
name: "SidebarExamples",
components: {
VueSidebar,
VueSidebarNav,
VueSidebarNavItem,
VueSidebarNavSubmenu,
VueSidebarNavPopoverSubmenu,
VuePopover,
Home,
Folder,
User,
Settings,
ChevronRight,
Command,
},
data() {
return {
sidebar1: {
isOpen: true,
isCollapsed: false,
},
sidebar2: {
isOpen: true,
isCollapsed: false,
},
sidebar3: {
isOpen: true,
isCollapsed: false,
},
sidebar4: {
isOpen: true,
},
sidebar5: {
isOpen: true,
isCollapsed: false,
activeRoute: "/dashboard",
},
};
},
mounted() {
// Inject nav button styles
const styleId = "sidebar-nav-styles";
if (!document.getElementById(styleId)) {
const style = document.createElement("style");
style.id = styleId;
style.textContent = `
/* Overrides VitePress theme - resets .vp-doc h2 styles */
ag-sidebar h2[slot="ag-header-start"] {
border-top: none !important;
padding-top: 0 !important;
padding-bottom: 0 !important;
margin-top: 0 !important;
margin-bottom: 0 !important;
letter-spacing: normal !important;
line-height: normal !important;
overflow-wrap: normal !important;
white-space: nowrap !important;
}
/* Ensure sidebars start in expanded mode in examples */
ag-sidebar:not([collapsed]) {
width: var(--ag-sidebar-width, 280px);
}
/* Fix disable-compact-mode sidebar visibility */
ag-sidebar[disable-compact-mode][open] {
display: flex !important;
visibility: visible !important;
opacity: 1 !important;
width: var(--ag-sidebar-width, 280px) !important;
transform: none !important;
}
/* Overrides VitePress theme - resets .vp-doc a styles */
ag-sidebar .nav-sublink {
font-weight: normal !important;
color: inherit !important;
text-decoration: none !important;
text-underline-offset: unset !important;
}
.nav-button {
display: flex;
align-items: center;
justify-content: center;
gap: var(--ag-space-3);
position: relative;
padding: var(--ag-space-2) var(--ag-space-3);
margin-block-end: var(--ag-space-1);
border: none;
background: none;
cursor: pointer;
width: 100%;
text-align: left;
border-radius: var(--ag-radius-sm);
transition: background var(--ag-fx-duration-sm);
color: inherit;
}
.nav-button svg {
flex-shrink: 0;
}
.nav-button:hover {
background: var(--ag-background-secondary);
}
.nav-button.active {
background: var(--ag-primary-background);
color: var(--ag-primary-text);
font-weight: 500;
}
.nav-button .chevron {
transition: transform var(--ag-fx-duration-md);
margin-left: auto;
}
.nav-button[aria-expanded="true"] .chevron {
transform: rotate(90deg);
}
.nav-button .collapsed-indicator {
display: none;
position: absolute;
bottom: -1px;
right: -1px;
width: var(--ag-space-3);
height: var(--ag-space-3);
}
.nav-button .collapsed-indicator svg {
color: var(--ag-text-muted);
transform: rotate(315deg);
}
.nav-button .nav-label {
flex-grow: 1;
overflow: hidden;
text-overflow: ellipsis;
}
.nav-button .nav-label,
.nav-button .chevron {
transition: opacity var(--ag-sidebar-transition-duration) var(--ag-sidebar-transition-easing);
white-space: nowrap;
}
ag-sidebar[collapsed] .nav-button {
width: auto;
padding: var(--ag-space-2);
}
ag-sidebar[collapsed] .nav-button .nav-label,
ag-sidebar[collapsed] .nav-button .chevron {
opacity: 0;
pointer-events: none;
display: none;
}
ag-sidebar[collapsed] .nav-button[aria-expanded] .collapsed-indicator {
display: block;
}
.nav-button-collapsed::part(ag-popover-body) {
padding: var(--ag-space-1);
}
ag-sidebar[collapsed] ag-sidebar-nav-submenu:not(.popover-submenu),
ag-sidebar:not([collapsed]) ag-popover,
ag-sidebar[collapsed] .nav-button-expanded,
ag-sidebar:not([collapsed]) .nav-button-collapsed {
display: none !important;
}
/* Fix popover centering in collapsed mode - ag-popover is inline-block by default */
ag-sidebar[collapsed] ag-popover.nav-button-collapsed {
display: block !important;
}
.nav-sublink {
display: block;
padding: var(--ag-space-2) var(--ag-space-3);
margin-block-end: var(--ag-space-1);
color: inherit;
text-decoration: none;
border-radius: var(--ag-radius-sm);
transition: background var(--ag-fx-duration-sm);
}
.nav-sublink:hover {
background: var(--ag-background-secondary);
}
/* Active state styles for navigation tracking */
.nav-button.active,
.nav-sublink.active {
background: var(--ag-primary-background);
color: var(--ag-primary-text);
font-weight: 500;
}
`;
document.head.appendChild(style);
}
},
methods: {
toggleSidebar1Collapse(e) {
const sidebar = e.target.closest("ag-sidebar");
if (sidebar && sidebar.toggleCollapse) {
sidebar.toggleCollapse();
}
},
toggleSidebar2Collapse(e) {
const sidebar = e.target.closest("ag-sidebar");
if (sidebar && sidebar.toggleCollapse) {
sidebar.toggleCollapse();
}
},
toggleSidebar4Responsive(e) {
const sidebar = e.target.closest("ag-sidebar");
if (sidebar && sidebar.toggleResponsive) {
sidebar.toggleResponsive();
}
},
handleSubmenuToggle(e) {
e.preventDefault();
e.stopPropagation();
const button = e.currentTarget;
const navItem = button.closest("ag-sidebar-nav-item");
const submenu = navItem?.querySelector("ag-sidebar-nav-submenu");
if (!submenu) return;
const currentAriaExpanded = button.getAttribute("aria-expanded");
const isCurrentlyExpanded = currentAriaExpanded === "true";
if (isCurrentlyExpanded) {
button.setAttribute("aria-expanded", "false");
submenu.removeAttribute("open");
} else {
button.setAttribute("aria-expanded", "true");
submenu.setAttribute("open", "");
}
},
toggleSidebar5Collapse(e) {
const sidebar = e.target.closest("ag-sidebar");
if (sidebar && sidebar.toggleCollapse) {
sidebar.toggleCollapse();
}
},
handleNavClick(route, e) {
e.preventDefault();
this.sidebar5.activeRoute = route;
const sidebar = e.target.closest("ag-sidebar");
// Update top-level nav buttons
const buttons = sidebar?.querySelectorAll(".nav-button");
buttons?.forEach((btn) => {
const isActive = btn.getAttribute("data-route") === route;
btn.classList.toggle("active", isActive);
if (isActive) {
btn.setAttribute("aria-current", "page");
} else {
btn.removeAttribute("aria-current");
}
});
// Update sublinks (both inline and in popovers)
const sublinks = sidebar?.querySelectorAll(".nav-sublink");
sublinks?.forEach((link) => {
const isActive = link.getAttribute("data-route") === route;
link.classList.toggle("active", isActive);
if (isActive) {
link.setAttribute("aria-current", "page");
} else {
link.removeAttribute("aria-current");
}
});
},
handleSettingsClick(e) {
this.handleNavClick("/settings", e);
this.handleSubmenuToggle(e);
},
},
};
</script>
Live Preview
View Lit / Web Component Code
import { LitElement, html } from 'lit';
import 'agnosticui-core/sidebar';
import 'agnosticui-core/sidebar-nav';
import 'agnosticui-core/popover';
export class SidebarLitExamples extends LitElement {
static properties = {
sidebar1: { type: Object },
sidebar2: { type: Object },
sidebar3: { type: Object },
sidebar4: { type: Object },
sidebar5: { type: Object },
};
constructor() {
super();
this.sidebar1 = { isOpen: true, isCollapsed: false };
this.sidebar2 = { isOpen: true, isCollapsed: false };
this.sidebar3 = { isOpen: true, isCollapsed: false };
this.sidebar4 = { isOpen: true };
this.sidebar5 = { isOpen: true, isCollapsed: false, activeRoute: '/dashboard' };
}
createRenderRoot() {
return this;
}
firstUpdated() {
// Inject critical sidebar navigation styles
const styleId = 'sidebar-nav-styles-lit';
if (!document.getElementById(styleId)) {
const style = document.createElement('style');
style.id = styleId;
style.textContent = `
/* CRITICAL: Sidebar component width - must be defined! */
ag-sidebar {
width: var(--ag-sidebar-width, 280px);
transition: width var(--ag-sidebar-transition-duration, 0.3s) var(--ag-sidebar-transition-easing, ease);
overflow: visible;
}
ag-sidebar[collapsed] {
width: 64px;
}
/* Fix disable-compact-mode sidebar visibility */
ag-sidebar[disable-compact-mode][open] {
display: flex !important;
visibility: visible !important;
opacity: 1 !important;
width: var(--ag-sidebar-width, 280px) !important;
transform: none !important;
}
/* Navigation button base styles */
.nav-button {
display: flex;
align-items: center;
gap: var(--ag-space-3);
position: relative;
padding: var(--ag-space-2) var(--ag-space-3);
margin-block-end: var(--ag-space-1);
border: none;
background: none;
cursor: pointer;
width: 100%;
text-align: left;
border-radius: var(--ag-radius-sm);
transition: background var(--ag-fx-duration-sm);
color: inherit;
}
.nav-button svg {
flex-shrink: 0;
}
.nav-button:hover {
background: var(--ag-background-secondary);
}
.nav-button.active {
background: var(--ag-primary-background);
color: var(--ag-primary-text);
font-weight: 500;
}
.nav-label {
flex-grow: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
transition: opacity var(--ag-sidebar-transition-duration) var(--ag-sidebar-transition-easing);
}
/* Chevron rotation for expanded submenus */
.chevron {
display: flex;
align-items: center;
transition: transform var(--ag-fx-duration-md), opacity var(--ag-fx-duration-sm);
margin-left: auto;
}
.nav-button[aria-expanded="true"] .chevron {
transform: rotate(90deg);
}
/* Collapsed indicator - small triangle at 4:30 position */
.collapsed-indicator {
display: none;
position: absolute;
bottom: -1px;
right: -1px;
width: var(--ag-space-3);
height: var(--ag-space-3);
}
.collapsed-indicator svg {
color: var(--ag-text-muted);
transform: rotate(315deg);
}
/* Show collapsed indicator in collapsed mode for buttons with submenus */
ag-sidebar[collapsed] .nav-button[aria-expanded] .collapsed-indicator {
display: block;
}
/* CRITICAL: Properly handle collapsed state */
ag-sidebar[collapsed] .nav-label,
ag-sidebar[collapsed] .chevron {
opacity: 0;
pointer-events: none;
display: none;
}
/* Center icons in collapsed mode */
ag-sidebar[collapsed] .nav-button {
width: auto;
padding: var(--ag-space-2);
}
/* Visibility rules for expanded vs collapsed mode */
ag-sidebar[collapsed] ag-sidebar-nav-submenu:not(.popover-submenu),
ag-sidebar:not([collapsed]) ag-popover,
ag-sidebar[collapsed] .nav-button-expanded,
ag-sidebar:not([collapsed]) .nav-button-collapsed {
display: none !important;
}
/* Fix popover centering in collapsed mode */
ag-sidebar[collapsed] ag-popover.nav-button-collapsed {
display: block !important;
}
/* CRITICAL: Submenu visibility - hidden by default, visible when open */
ag-sidebar-nav-submenu {
display: none;
overflow: hidden;
}
ag-sidebar-nav-submenu[open] {
display: block;
}
/* Submenu link styles */
.nav-sublink {
display: block;
padding: var(--ag-space-2) var(--ag-space-3);
margin-block-end: var(--ag-space-1);
color: inherit;
text-decoration: none;
border-radius: var(--ag-radius-sm);
transition: background var(--ag-fx-duration-sm);
}
.nav-sublink:hover {
background: var(--ag-background-secondary);
}
/* Active state styles for navigation tracking */
.nav-button.active,
.nav-sublink.active {
background: var(--ag-primary-background);
color: var(--ag-primary-text);
font-weight: 500;
}
/* Popover submenu styles */
.nav-button-collapsed::part(ag-popover-body) {
padding: var(--ag-space-1);
}
/* Hide popover header for nav popovers */
.nav-button-collapsed::part(ag-popover-header) {
display: none;
}
`;
document.head.appendChild(style);
}
// Set up event listeners for submenu toggles
this.setupSubmenuToggles();
}
setupSubmenuToggles() {
const buttons = this.querySelectorAll('.nav-button-expanded');
buttons.forEach((button) => {
button.addEventListener('click', (e) => {
e.preventDefault();
e.stopPropagation();
const navItem = button.closest('ag-sidebar-nav-item');
const submenu = navItem?.querySelector('ag-sidebar-nav-submenu');
if (!submenu) return;
const currentAriaExpanded = button.getAttribute('aria-expanded');
const isCurrentlyExpanded = currentAriaExpanded === 'true';
if (isCurrentlyExpanded) {
button.setAttribute('aria-expanded', 'false');
submenu.removeAttribute('open');
} else {
button.setAttribute('aria-expanded', 'true');
submenu.setAttribute('open', '');
}
});
});
}
toggleSidebar1Collapse(e) {
const sidebar = e.target.closest('ag-sidebar');
if (sidebar && sidebar.toggleCollapse) {
sidebar.toggleCollapse();
}
}
toggleSidebar2Collapse(e) {
const sidebar = e.target.closest('ag-sidebar');
if (sidebar && sidebar.toggleCollapse) {
sidebar.toggleCollapse();
}
}
toggleSidebar4Responsive(e) {
const sidebar = e.target.closest('ag-sidebar');
if (sidebar && sidebar.toggleResponsive) {
sidebar.toggleResponsive();
}
}
toggleSidebar5Collapse(e) {
const sidebar = e.target.closest('ag-sidebar');
if (sidebar && sidebar.toggleCollapse) {
sidebar.toggleCollapse();
}
}
handleNavClick(route, e) {
e.preventDefault();
this.sidebar5.activeRoute = route;
this.requestUpdate();
const sidebar = e.target.closest('ag-sidebar');
// Update top-level nav buttons
const buttons = sidebar?.querySelectorAll('.nav-button');
buttons?.forEach((btn) => {
const isActive = btn.getAttribute('data-route') === route;
btn.classList.toggle('active', isActive);
if (isActive) {
btn.setAttribute('aria-current', 'page');
} else {
btn.removeAttribute('aria-current');
}
});
// Update sublinks (both inline and in popovers)
const sublinks = sidebar?.querySelectorAll('.nav-sublink');
sublinks?.forEach((link) => {
const isActive = link.getAttribute('data-route') === route;
link.classList.toggle('active', isActive);
if (isActive) {
link.setAttribute('aria-current', 'page');
} else {
link.removeAttribute('aria-current');
}
});
}
handleSettingsClick(e) {
this.handleNavClick('/settings', e);
// Also toggle the submenu
const button = e.currentTarget;
const navItem = button.closest('ag-sidebar-nav-item');
const submenu = navItem?.querySelector('ag-sidebar-nav-submenu');
if (!submenu) return;
const currentAriaExpanded = button.getAttribute('aria-expanded');
const isCurrentlyExpanded = currentAriaExpanded === 'true';
if (isCurrentlyExpanded) {
button.setAttribute('aria-expanded', 'false');
submenu.removeAttribute('open');
} else {
button.setAttribute('aria-expanded', 'true');
submenu.setAttribute('open', '');
}
}
render() {
return html`
<section>
<div class="mbe4">
<h2>Default Sidebar with Navigation</h2>
<p>
Demonstrates the sidebar with navigation items, submenus, and both expanded/collapsed states.
Click the header toggle to collapse into rail mode (icon-only).
</p>
</div>
<div class="mbe6">
<div style="position: relative; display: flex; height: 500px; border: 1px solid var(--ag-border-color); border-radius: 0.5rem; overflow: hidden; contain: layout;">
<ag-sidebar
?open="${this.sidebar1.isOpen}"
?collapsed="${this.sidebar1.isCollapsed}"
show-mobile-toggle
@toggle="${(e) => { this.sidebar1.isOpen = e.detail; this.requestUpdate(); }}"
@collapse="${(e) => { this.sidebar1.isCollapsed = e.detail; this.requestUpdate(); }}"
>
<h2
slot="ag-header-start"
style="margin: 0; font-size: 1.125rem; font-weight: 600;"
>
Dashboard
</h2>
<button
type="button"
slot="ag-header-toggle"
@click="${this.toggleSidebar1Collapse}"
style="background: none; border: none; padding: 8px 0; cursor: pointer; display: flex; align-items: center; color: inherit;"
aria-label="Toggle sidebar"
>
<svg
width="20"
height="20"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<rect width="18" height="18" x="3" y="3" rx="2" />
<path d="M9 3v18" />
</svg>
</button>
<ag-sidebar-nav>
<ag-sidebar-nav-item>
<button
type="button"
class="nav-button active"
aria-current="page"
>
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="m3 9 9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/>
<polyline points="9 22 9 12 15 12 15 22"/>
</svg>
<span class="nav-label">Dashboard</span>
</button>
</ag-sidebar-nav-item>
<ag-sidebar-nav-item>
<button
type="button"
class="nav-button nav-button-expanded"
aria-expanded="false"
>
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/>
</svg>
<span class="nav-label">Projects</span>
<span class="chevron">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<polyline points="9 18 15 12 9 6"/>
</svg>
</span>
</button>
<!-- Popover for COLLAPSED mode -->
<ag-popover
class="nav-button-collapsed"
placement="right-start"
trigger-type="click"
distance="8"
arrow
.showHeader="${false}"
>
<button
slot="trigger"
type="button"
class="nav-button"
>
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/>
</svg>
<span class="nav-label">Projects</span>
<span class="collapsed-indicator">
<svg viewBox="0 0 8 8" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M2 3l2 2 2-2" stroke="currentColor" stroke-width="1" stroke-linecap="round"/>
</svg>
</span>
</button>
<ag-sidebar-nav-popover-submenu
slot="content"
class="popover-submenu"
>
<a href="#" class="nav-sublink" @click="${(e) => e.preventDefault()}">Project Alpha</a>
<a href="#" class="nav-sublink" @click="${(e) => e.preventDefault()}">Project Beta</a>
<a href="#" class="nav-sublink" @click="${(e) => e.preventDefault()}">Project Gamma</a>
</ag-sidebar-nav-popover-submenu>
</ag-popover>
<!-- Inline submenu for expanded mode -->
<ag-sidebar-nav-submenu>
<a class="nav-sublink" href="#" @click="${(e) => e.preventDefault()}">Project Alpha</a>
<a class="nav-sublink" href="#" @click="${(e) => e.preventDefault()}">Project Beta</a>
<a class="nav-sublink" href="#" @click="${(e) => e.preventDefault()}">Project Gamma</a>
</ag-sidebar-nav-submenu>
</ag-sidebar-nav-item>
<ag-sidebar-nav-item>
<button
type="button"
class="nav-button"
>
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"/>
<circle cx="9" cy="7" r="4"/>
<path d="M22 21v-2a4 4 0 0 0-3-3.87"/>
<path d="M16 3.13a4 4 0 0 1 0 7.75"/>
</svg>
<span class="nav-label">Team</span>
</button>
</ag-sidebar-nav-item>
<ag-sidebar-nav-item>
<button
type="button"
class="nav-button nav-button-expanded"
aria-expanded="false"
>
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z"/>
<circle cx="12" cy="12" r="3"/>
</svg>
<span class="nav-label">Settings</span>
<span class="chevron">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<polyline points="9 18 15 12 9 6"/>
</svg>
</span>
</button>
<!-- Popover for COLLAPSED mode -->
<ag-popover
class="nav-button-collapsed"
placement="right-start"
trigger-type="click"
distance="8"
arrow
.showHeader="${false}"
>
<button
slot="trigger"
type="button"
class="nav-button"
>
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z"/>
<circle cx="12" cy="12" r="3"/>
</svg>
<span class="nav-label">Settings</span>
<span class="collapsed-indicator">
<svg viewBox="0 0 8 8" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M2 3l2 2 2-2" stroke="currentColor" stroke-width="1" stroke-linecap="round"/>
</svg>
</span>
</button>
<ag-sidebar-nav-popover-submenu
slot="content"
class="popover-submenu"
>
<a href="#" class="nav-sublink" @click="${(e) => e.preventDefault()}">Profile</a>
<a href="#" class="nav-sublink" @click="${(e) => e.preventDefault()}">Billing</a>
<a href="#" class="nav-sublink" @click="${(e) => e.preventDefault()}">Security</a>
<a href="#" class="nav-sublink" @click="${(e) => e.preventDefault()}">Preferences</a>
</ag-sidebar-nav-popover-submenu>
</ag-popover>
<ag-sidebar-nav-submenu>
<a class="nav-sublink" href="#" @click="${(e) => e.preventDefault()}">Profile</a>
<a class="nav-sublink" href="#" @click="${(e) => e.preventDefault()}">Billing</a>
<a class="nav-sublink" href="#" @click="${(e) => e.preventDefault()}">Security</a>
<a class="nav-sublink" href="#" @click="${(e) => e.preventDefault()}">Preferences</a>
</ag-sidebar-nav-submenu>
</ag-sidebar-nav-item>
</ag-sidebar-nav>
<div
slot="ag-footer"
style="font-size: 0.875rem; color: var(--ag-text-secondary);"
>
© 2024 Company
</div>
</ag-sidebar>
<main style="flex: 1; padding: 2rem; overflow: auto; background: var(--ag-background);">
<h1 style="margin-top: 0;">Main Content</h1>
<p>Click the header toggle to collapse the sidebar into rail mode.</p>
<p>When collapsed, hover over items with submenus to see them in popovers.</p>
</main>
</div>
</div>
<div class="mbe4">
<h2>Sidebar with Header Actions</h2>
<p>
Demonstrates using <code>ag-header-start</code>, <code>ag-header-end</code>, and <code>ag-header-toggle</code> slots
for a composable header layout with action buttons.
</p>
</div>
<div class="mbe6">
<div style="position: relative; display: flex; height: 500px; border: 1px solid var(--ag-border-color); border-radius: 0.5rem; overflow: hidden; contain: layout;">
<ag-sidebar
?open="${this.sidebar2.isOpen}"
?collapsed="${this.sidebar2.isCollapsed}"
show-mobile-toggle
@toggle="${(e) => { this.sidebar2.isOpen = e.detail; this.requestUpdate(); }}"
@collapse="${(e) => { this.sidebar2.isCollapsed = e.detail; this.requestUpdate(); }}"
>
<h2
slot="ag-header-start"
style="margin: 0; font-size: 1.125rem; font-weight: 600;"
>
My Application
</h2>
<button
type="button"
slot="ag-header-end"
@click="${() => {}}"
style="background: none; border: none; padding: 8px; cursor: pointer; display: flex; align-items: center; color: inherit; border-radius: 0.25rem;"
aria-label="Settings"
title="Settings"
>
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z"/>
<circle cx="12" cy="12" r="3"/>
</svg>
</button>
<button
type="button"
slot="ag-header-toggle"
@click="${this.toggleSidebar2Collapse}"
style="background: none; border: none; padding: 8px 0; cursor: pointer; display: flex; align-items: center; color: inherit;"
aria-label="Toggle sidebar"
>
<svg
width="20"
height="20"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<rect width="18" height="18" x="3" y="3" rx="2" />
<path d="M9 3v18" />
</svg>
</button>
<ag-sidebar-nav>
<ag-sidebar-nav-item>
<button
type="button"
class="nav-button active"
aria-current="page"
>
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="m3 9 9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/>
<polyline points="9 22 9 12 15 12 15 22"/>
</svg>
<span class="nav-label">Dashboard</span>
</button>
</ag-sidebar-nav-item>
<ag-sidebar-nav-item>
<button
type="button"
class="nav-button"
>
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/>
</svg>
<span class="nav-label">Projects</span>
</button>
</ag-sidebar-nav-item>
<ag-sidebar-nav-item>
<button
type="button"
class="nav-button"
>
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"/>
<circle cx="9" cy="7" r="4"/>
<path d="M22 21v-2a4 4 0 0 0-3-3.87"/>
<path d="M16 3.13a4 4 0 0 1 0 7.75"/>
</svg>
<span class="nav-label">Team</span>
</button>
</ag-sidebar-nav-item>
</ag-sidebar-nav>
<div
slot="ag-footer"
style="font-size: 0.875rem; color: var(--ag-text-secondary);"
>
© 2024 Company
</div>
</ag-sidebar>
<main style="flex: 1; padding: 2rem; overflow: auto; background: var(--ag-background);">
<h1 style="margin-top: 0;">Header Actions Pattern</h1>
<p>The header includes a settings button in the <code>ag-header-end</code> slot.</p>
<p>This allows for flexible header layouts with multiple action buttons.</p>
</main>
</div>
</div>
<div class="mbe4">
<h2>Sidebar with Built-in Toggle</h2>
<p>
Using <code>show-header-toggle</code> adds a built-in collapse button automatically.
</p>
</div>
<div class="mbe6">
<div style="position: relative; display: flex; height: 500px; border: 1px solid var(--ag-border-color); border-radius: 0.5rem; overflow: hidden; contain: layout;">
<ag-sidebar
?open="${this.sidebar3.isOpen}"
?collapsed="${this.sidebar3.isCollapsed}"
show-header-toggle
show-mobile-toggle
@toggle="${(e) => { this.sidebar3.isOpen = e.detail; this.requestUpdate(); }}"
@collapse="${(e) => { this.sidebar3.isCollapsed = e.detail; this.requestUpdate(); }}"
>
<h2
slot="ag-header-start"
style="margin: 0; font-size: 1.125rem; font-weight: 600;"
>
Built-in Toggle
</h2>
<ag-sidebar-nav>
<ag-sidebar-nav-item>
<button
type="button"
class="nav-button active"
aria-current="page"
>
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="m3 9 9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/>
<polyline points="9 22 9 12 15 12 15 22"/>
</svg>
<span class="nav-label">Dashboard</span>
</button>
</ag-sidebar-nav-item>
<ag-sidebar-nav-item>
<button
type="button"
class="nav-button"
>
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/>
</svg>
<span class="nav-label">Projects</span>
</button>
</ag-sidebar-nav-item>
<ag-sidebar-nav-item>
<button
type="button"
class="nav-button"
>
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"/>
<circle cx="9" cy="7" r="4"/>
<path d="M22 21v-2a4 4 0 0 0-3-3.87"/>
<path d="M16 3.13a4 4 0 0 1 0 7.75"/>
</svg>
<span class="nav-label">Team</span>
</button>
</ag-sidebar-nav-item>
</ag-sidebar-nav>
<div
slot="ag-footer"
style="font-size: 0.875rem; color: var(--ag-text-secondary);"
>
© 2024 Company
</div>
</ag-sidebar>
<main style="flex: 1; padding: 2rem; overflow: auto; background: var(--ag-background);">
<h1 style="margin-top: 0;">Built-in Header Toggle</h1>
<p>No need to provide a custom toggle button—the sidebar includes one automatically.</p>
</main>
</div>
</div>
<div class="mbe4">
<h2>Disable Compact Mode</h2>
<p>
With <code>disable-compact-mode</code>, the sidebar has no intermediate collapsed/rail state.
It's either fully open (expanded) or completely hidden. This pattern is used in applications like Claude AI Studio.
</p>
</div>
<div class="mbe6">
<div style="position: relative; display: flex; height: 500px; border: 1px solid var(--ag-border-color); border-radius: 0.5rem; overflow: hidden; contain: layout;">
<ag-sidebar
?open="${this.sidebar4.isOpen}"
disable-compact-mode
show-mobile-toggle
@toggle="${(e) => { this.sidebar4.isOpen = e.detail; this.requestUpdate(); }}"
>
<h2
slot="ag-header-start"
style="margin: 0; font-size: 1.125rem; font-weight: 600;"
>
AI Studio
</h2>
<button
type="button"
slot="ag-header-toggle"
@click="${this.toggleSidebar4Responsive}"
style="background: none; border: none; padding: 8px 0; cursor: pointer; display: flex; align-items: center; color: inherit;"
aria-label="Toggle sidebar"
>
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M18 3a3 3 0 0 0-3 3v12a3 3 0 0 0 3 3 3 3 0 0 0 3-3 3 3 0 0 0-3-3H6a3 3 0 0 0-3 3 3 3 0 0 0 3 3 3 3 0 0 0 3-3V6a3 3 0 0 0-3-3 3 3 0 0 0-3 3 3 3 0 0 0 3 3h12a3 3 0 0 0 3-3 3 3 0 0 0-3-3z"/>
</svg>
</button>
<ag-sidebar-nav>
<ag-sidebar-nav-item>
<button
type="button"
class="nav-button active"
aria-current="page"
>
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="m3 9 9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/>
<polyline points="9 22 9 12 15 12 15 22"/>
</svg>
<span class="nav-label">Dashboard</span>
</button>
</ag-sidebar-nav-item>
<ag-sidebar-nav-item>
<button
type="button"
class="nav-button"
>
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/>
</svg>
<span class="nav-label">Projects</span>
</button>
</ag-sidebar-nav-item>
<ag-sidebar-nav-item>
<button
type="button"
class="nav-button"
>
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"/>
<circle cx="9" cy="7" r="4"/>
<path d="M22 21v-2a4 4 0 0 0-3-3.87"/>
<path d="M16 3.13a4 4 0 0 1 0 7.75"/>
</svg>
<span class="nav-label">Team</span>
</button>
</ag-sidebar-nav-item>
<ag-sidebar-nav-item>
<button
type="button"
class="nav-button"
>
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z"/>
<circle cx="12" cy="12" r="3"/>
</svg>
<span class="nav-label">Settings</span>
</button>
</ag-sidebar-nav-item>
</ag-sidebar-nav>
<div
slot="ag-footer"
style="font-size: 0.875rem; color: var(--ag-text-secondary);"
>
© 2024 Company
</div>
</ag-sidebar>
<main style="flex: 1; padding: 2rem; overflow: auto; background: var(--ag-background);">
<h1 style="margin-top: 0;">Disable Compact Mode</h1>
<p>Use the mobile toggle button to show/hide the sidebar.</p>
<p>Notice there's no collapsed/rail mode—it's either fully visible or completely hidden.</p>
</main>
</div>
</div>
<div class="mbe4">
<h2>Active Item Tracking</h2>
<p>
Click navigation items to see the active state change. This demonstrates how to track the current route
and apply active styling to both top-level and submenu items.
</p>
</div>
<div class="mbe6">
<div style="position: relative; display: flex; height: 500px; border: 1px solid var(--ag-border-color); border-radius: 0.5rem; overflow: hidden; contain: layout;">
<ag-sidebar
?open="${this.sidebar5.isOpen}"
?collapsed="${this.sidebar5.isCollapsed}"
show-mobile-toggle
@toggle="${(e) => { this.sidebar5.isOpen = e.detail; this.requestUpdate(); }}"
@collapse="${(e) => { this.sidebar5.isCollapsed = e.detail; this.requestUpdate(); }}"
>
<h2
slot="ag-header-start"
style="margin: 0; font-size: 1.125rem; font-weight: 600;"
>
Navigation
</h2>
<button
type="button"
slot="ag-header-toggle"
@click="${this.toggleSidebar5Collapse}"
style="background: none; border: none; padding: 8px 0; cursor: pointer; display: flex; align-items: center; color: inherit;"
aria-label="Toggle sidebar"
>
<svg
width="20"
height="20"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<rect width="18" height="18" x="3" y="3" rx="2" />
<path d="M9 3v18" />
</svg>
</button>
<ag-sidebar-nav>
<ag-sidebar-nav-item>
<button
type="button"
class="nav-button active"
aria-current="page"
data-route="/dashboard"
@click="${(e) => this.handleNavClick('/dashboard', e)}"
>
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="m3 9 9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/>
<polyline points="9 22 9 12 15 12 15 22"/>
</svg>
<span class="nav-label">Dashboard</span>
</button>
</ag-sidebar-nav-item>
<ag-sidebar-nav-item>
<button
type="button"
class="nav-button"
data-route="/projects"
@click="${(e) => this.handleNavClick('/projects', e)}"
>
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/>
</svg>
<span class="nav-label">Projects</span>
</button>
</ag-sidebar-nav-item>
<ag-sidebar-nav-item>
<button
type="button"
class="nav-button"
data-route="/team"
@click="${(e) => this.handleNavClick('/team', e)}"
>
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"/>
<circle cx="9" cy="7" r="4"/>
<path d="M22 21v-2a4 4 0 0 0-3-3.87"/>
<path d="M16 3.13a4 4 0 0 1 0 7.75"/>
</svg>
<span class="nav-label">Team</span>
</button>
</ag-sidebar-nav-item>
<ag-sidebar-nav-item>
<button
type="button"
class="nav-button nav-button-expanded"
aria-expanded="false"
data-route="/settings"
@click="${this.handleSettingsClick}"
>
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z"/>
<circle cx="12" cy="12" r="3"/>
</svg>
<span class="nav-label">Settings</span>
<span class="chevron">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<polyline points="9 18 15 12 9 6"/>
</svg>
</span>
</button>
<ag-popover
class="nav-button-collapsed"
placement="right-start"
trigger-type="click"
distance="8"
arrow
.showHeader="${false}"
>
<button
slot="trigger"
type="button"
class="nav-button"
data-route="/settings"
@click="${(e) => this.handleNavClick('/settings', e)}"
>
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z"/>
<circle cx="12" cy="12" r="3"/>
</svg>
<span class="nav-label">Settings</span>
<span class="collapsed-indicator">
<svg viewBox="0 0 8 8" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M2 3l2 2 2-2" stroke="currentColor" stroke-width="1" stroke-linecap="round"/>
</svg>
</span>
</button>
<ag-sidebar-nav-popover-submenu
slot="content"
class="popover-submenu"
>
<a
href="#"
class="nav-sublink"
data-route="/settings/profile"
@click="${(e) => this.handleNavClick('/settings/profile', e)}"
>Profile</a>
<a
href="#"
class="nav-sublink"
data-route="/settings/billing"
@click="${(e) => this.handleNavClick('/settings/billing', e)}"
>Billing</a>
<a
href="#"
class="nav-sublink"
data-route="/settings/security"
@click="${(e) => this.handleNavClick('/settings/security', e)}"
>Security</a>
</ag-sidebar-nav-popover-submenu>
</ag-popover>
<ag-sidebar-nav-submenu>
<a
class="nav-sublink"
href="#"
data-route="/settings/profile"
@click="${(e) => this.handleNavClick('/settings/profile', e)}"
>Profile</a>
<a
class="nav-sublink"
href="#"
data-route="/settings/billing"
@click="${(e) => this.handleNavClick('/settings/billing', e)}"
>Billing</a>
<a
class="nav-sublink"
href="#"
data-route="/settings/security"
@click="${(e) => this.handleNavClick('/settings/security', e)}"
>Security</a>
</ag-sidebar-nav-submenu>
</ag-sidebar-nav-item>
</ag-sidebar-nav>
</ag-sidebar>
<main style="flex: 1; padding: 2rem; overflow: auto; background: var(--ag-background);">
<h1 style="margin-top: 0;">Active Item Tracking</h1>
<p>Current route: <strong>${this.sidebar5.activeRoute}</strong></p>
<p>Click navigation items to see the active state change.</p>
<ul>
<li><strong>Active styling:</strong> Background color and font weight change</li>
<li><strong>ARIA current:</strong> The active item has aria-current="page" for accessibility</li>
<li><strong>Submenu support:</strong> Sublinks also track active state</li>
<li><strong>Popover sync:</strong> Active state works in both inline and popover modes</li>
</ul>
<p>
In a real application, you'd integrate this with your router (Vue Router, etc.) to automatically
update the active state based on the current route.
</p>
</main>
</div>
</div>
</section>
`;
}
}
customElements.define('sidebar-lit-examples', SidebarLitExamples);
Interactive Preview: Click the "Open in StackBlitz" button below to see this example running live in an interactive playground.
View React Code
import { ReactSidebar } from "agnosticui-core/sidebar/react";
import {
ReactSidebarNav,
ReactSidebarNavItem,
ReactSidebarNavSubmenu,
ReactSidebarNavPopoverSubmenu,
} from "agnosticui-core/sidebar-nav/react";
import { ReactPopover } from "agnosticui-core/popover/react";
import { useState, useEffect } from "react";
// SVG Icons as inline components
const HomeIcon = () => (
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="m3 9 9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/>
<polyline points="9 22 9 12 15 12 15 22"/>
</svg>
);
const FolderIcon = () => (
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/>
</svg>
);
const UserIcon = () => (
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"/>
<circle cx="9" cy="7" r="4"/>
<path d="M22 21v-2a4 4 0 0 0-3-3.87"/>
<path d="M16 3.13a4 4 0 0 1 0 7.75"/>
</svg>
);
const SettingsIcon = () => (
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z"/>
<circle cx="12" cy="12" r="3"/>
</svg>
);
const ChevronRightIcon = () => (
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<polyline points="9 18 15 12 9 6"/>
</svg>
);
const PanelIcon = () => (
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<rect width="18" height="18" x="3" y="3" rx="2" />
<path d="M9 3v18" />
</svg>
);
const CommandIcon = () => (
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M18 3a3 3 0 0 0-3 3v12a3 3 0 0 0 3 3 3 3 0 0 0 3-3 3 3 0 0 0-3-3H6a3 3 0 0 0-3 3 3 3 0 0 0 3 3 3 3 0 0 0 3-3V6a3 3 0 0 0-3-3 3 3 0 0 0-3 3 3 3 0 0 0 3 3h12a3 3 0 0 0 3-3 3 3 0 0 0-3-3z"/>
</svg>
);
export default function SidebarReactExamples() {
const [sidebar1, setSidebar1] = useState({ isOpen: true, isCollapsed: false });
const [sidebar2, setSidebar2] = useState({ isOpen: true, isCollapsed: false });
const [sidebar3, setSidebar3] = useState({ isOpen: true, isCollapsed: false });
const [sidebar4, setSidebar4] = useState({ isOpen: true });
const [sidebar5, setSidebar5] = useState({ isOpen: true, isCollapsed: false, activeRoute: '/dashboard' });
useEffect(() => {
// Inject critical sidebar navigation styles
const styleId = 'sidebar-nav-styles-react';
if (!document.getElementById(styleId)) {
const style = document.createElement('style');
style.id = styleId;
style.textContent = `
/* CRITICAL: Sidebar component width - must be defined! */
ag-sidebar {
width: var(--ag-sidebar-width, 280px);
transition: width var(--ag-sidebar-transition-duration, 0.3s) var(--ag-sidebar-transition-easing, ease);
overflow: visible;
}
ag-sidebar[collapsed] {
width: 64px;
}
/* Fix disable-compact-mode sidebar visibility */
ag-sidebar[disable-compact-mode][open] {
display: flex !important;
visibility: visible !important;
opacity: 1 !important;
width: var(--ag-sidebar-width, 280px) !important;
transform: none !important;
}
/* Navigation button base styles */
.nav-button {
display: flex;
align-items: center;
gap: var(--ag-space-3);
position: relative;
padding: var(--ag-space-2) var(--ag-space-3);
margin-block-end: var(--ag-space-1);
border: none;
background: none;
cursor: pointer;
width: 100%;
text-align: left;
border-radius: var(--ag-radius-sm);
transition: background var(--ag-fx-duration-sm);
color: inherit;
}
.nav-button svg {
flex-shrink: 0;
}
.nav-button:hover {
background: var(--ag-background-secondary);
}
.nav-button.active {
background: var(--ag-primary-background);
color: var(--ag-primary-text);
font-weight: 500;
}
.nav-label {
flex-grow: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
transition: opacity var(--ag-sidebar-transition-duration) var(--ag-sidebar-transition-easing);
}
/* Chevron rotation for expanded submenus */
.chevron {
display: flex;
align-items: center;
transition: transform var(--ag-fx-duration-md), opacity var(--ag-fx-duration-sm);
margin-left: auto;
}
.nav-button[aria-expanded="true"] .chevron {
transform: rotate(90deg);
}
/* Collapsed indicator - small triangle at 4:30 position */
.collapsed-indicator {
display: none;
position: absolute;
bottom: -1px;
right: -1px;
width: var(--ag-space-3);
height: var(--ag-space-3);
}
.collapsed-indicator svg {
color: var(--ag-text-muted);
transform: rotate(315deg);
}
/* Show collapsed indicator in collapsed mode for buttons with submenus */
ag-sidebar[collapsed] .nav-button[aria-expanded] .collapsed-indicator {
display: block;
}
/* CRITICAL: Properly handle collapsed state */
ag-sidebar[collapsed] .nav-label,
ag-sidebar[collapsed] .chevron {
opacity: 0;
pointer-events: none;
display: none;
}
/* Center icons in collapsed mode */
ag-sidebar[collapsed] .nav-button {
width: auto;
padding: var(--ag-space-2);
}
/* Visibility rules for expanded vs collapsed mode */
ag-sidebar[collapsed] ag-sidebar-nav-submenu:not(.popover-submenu),
ag-sidebar:not([collapsed]) ag-popover,
ag-sidebar[collapsed] .nav-button-expanded,
ag-sidebar:not([collapsed]) .nav-button-collapsed {
display: none !important;
}
/* Fix popover centering in collapsed mode */
ag-sidebar[collapsed] ag-popover.nav-button-collapsed {
display: block !important;
}
/* CRITICAL: Submenu visibility - hidden by default, visible when open */
ag-sidebar-nav-submenu {
display: none;
overflow: hidden;
}
ag-sidebar-nav-submenu[open] {
display: block;
}
/* Submenu link styles */
.nav-sublink {
display: block;
padding: var(--ag-space-2) var(--ag-space-3);
margin-block-end: var(--ag-space-1);
color: inherit;
text-decoration: none;
border-radius: var(--ag-radius-sm);
transition: background var(--ag-fx-duration-sm);
}
.nav-sublink:hover {
background: var(--ag-background-secondary);
}
/* Active state styles for navigation tracking */
.nav-button.active,
.nav-sublink.active {
background: var(--ag-primary-background);
color: var(--ag-primary-text);
font-weight: 500;
}
/* Popover submenu styles */
.nav-button-collapsed::part(ag-popover-body) {
padding: var(--ag-space-1);
}
/* Hide popover header for nav popovers */
.nav-button-collapsed::part(ag-popover-header) {
display: none;
}
`;
document.head.appendChild(style);
}
// Set up event listeners for submenu toggles
const buttons = document.querySelectorAll('.nav-button-expanded');
buttons.forEach((button) => {
button.addEventListener('click', (e) => {
e.preventDefault();
e.stopPropagation();
const navItem = button.closest('ag-sidebar-nav-item');
const submenu = navItem?.querySelector('ag-sidebar-nav-submenu');
if (!submenu) return;
const currentAriaExpanded = button.getAttribute('aria-expanded');
const isCurrentlyExpanded = currentAriaExpanded === 'true';
if (isCurrentlyExpanded) {
button.setAttribute('aria-expanded', 'false');
submenu.removeAttribute('open');
} else {
button.setAttribute('aria-expanded', 'true');
submenu.setAttribute('open', '');
}
});
});
}, []);
const toggleSidebar1Collapse = () => {
setSidebar1({ ...sidebar1, isCollapsed: !sidebar1.isCollapsed });
};
const toggleSidebar2Collapse = () => {
setSidebar2({ ...sidebar2, isCollapsed: !sidebar2.isCollapsed });
};
const toggleSidebar4Responsive = () => {
setSidebar4({ ...sidebar4, isOpen: !sidebar4.isOpen });
};
const toggleSidebar5Collapse = () => {
setSidebar5({ ...sidebar5, isCollapsed: !sidebar5.isCollapsed });
};
const handleNavClick = (route, e) => {
e.preventDefault();
setSidebar5({ ...sidebar5, activeRoute: route });
const sidebar = e.target.closest('ag-sidebar');
// Update top-level nav buttons
const buttons = sidebar?.querySelectorAll('.nav-button');
buttons?.forEach((btn) => {
const isActive = btn.getAttribute('data-route') === route;
btn.classList.toggle('active', isActive);
if (isActive) {
btn.setAttribute('aria-current', 'page');
} else {
btn.removeAttribute('aria-current');
}
});
// Update sublinks (both inline and in popovers)
const sublinks = sidebar?.querySelectorAll('.nav-sublink');
sublinks?.forEach((link) => {
const isActive = link.getAttribute('data-route') === route;
link.classList.toggle('active', isActive);
if (isActive) {
link.setAttribute('aria-current', 'page');
} else {
link.removeAttribute('aria-current');
}
});
};
const handleSettingsClick = (e) => {
handleNavClick('/settings', e);
// Also toggle the submenu
const button = e.currentTarget;
const navItem = button.closest('ag-sidebar-nav-item');
const submenu = navItem?.querySelector('ag-sidebar-nav-submenu');
if (!submenu) return;
const currentAriaExpanded = button.getAttribute('aria-expanded');
const isCurrentlyExpanded = currentAriaExpanded === 'true';
if (isCurrentlyExpanded) {
button.setAttribute('aria-expanded', 'false');
submenu.removeAttribute('open');
} else {
button.setAttribute('aria-expanded', 'true');
submenu.setAttribute('open', '');
}
};
return (
<section>
<div className="mbe4">
<h2>Default Sidebar with Navigation</h2>
<p>
Demonstrates the sidebar with navigation items, submenus, and both expanded/collapsed states.
Click the header toggle to collapse into rail mode (icon-only).
</p>
</div>
<div className="mbe6">
<div style={{ position: 'relative', display: 'flex', height: '500px', border: '1px solid var(--ag-border-color)', borderRadius: '0.5rem', overflow: 'hidden', contain: 'layout' }}>
<ReactSidebar
open={sidebar1.isOpen}
collapsed={sidebar1.isCollapsed}
showMobileToggle={true}
onToggle={(open) => setSidebar1({ ...sidebar1, isOpen: open })}
onCollapse={(collapsed) => setSidebar1({ ...sidebar1, isCollapsed: collapsed })}
>
<h2
slot="ag-header-start"
style={{ margin: 0, fontSize: '1.125rem', fontWeight: 600 }}
>
Dashboard
</h2>
<button
type="button"
slot="ag-header-toggle"
onClick={toggleSidebar1Collapse}
style={{ background: 'none', border: 'none', padding: '8px 0', cursor: 'pointer', display: 'flex', alignItems: 'center', color: 'inherit' }}
aria-label="Toggle sidebar"
>
<PanelIcon />
</button>
<ReactSidebarNav>
<ReactSidebarNavItem>
<button
type="button"
className="nav-button active"
aria-current="page"
>
<HomeIcon />
<span className="nav-label">Dashboard</span>
</button>
</ReactSidebarNavItem>
<ReactSidebarNavItem>
<button
type="button"
className="nav-button nav-button-expanded"
aria-expanded="false"
>
<FolderIcon />
<span className="nav-label">Projects</span>
<span className="chevron"><ChevronRightIcon /></span>
</button>
{/* Popover for COLLAPSED mode */}
<ReactPopover
className="nav-button-collapsed"
placement="right-start"
triggerType="click"
distance={8}
arrow={true}
>
<button
slot="trigger"
type="button"
className="nav-button"
>
<FolderIcon />
<span className="nav-label">Projects</span>
<span className="collapsed-indicator">
<svg viewBox="0 0 8 8" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M2 3l2 2 2-2" stroke="currentColor" strokeWidth="1" strokeLinecap="round"/>
</svg>
</span>
</button>
<ReactSidebarNavPopoverSubmenu
slot="content"
className="popover-submenu"
>
<a href="#" className="nav-sublink" onClick={(e) => e.preventDefault()}>Project Alpha</a>
<a href="#" className="nav-sublink" onClick={(e) => e.preventDefault()}>Project Beta</a>
<a href="#" className="nav-sublink" onClick={(e) => e.preventDefault()}>Project Gamma</a>
</ReactSidebarNavPopoverSubmenu>
</ReactPopover>
{/* Inline submenu for expanded mode */}
<ReactSidebarNavSubmenu>
<a className="nav-sublink" href="#" onClick={(e) => e.preventDefault()}>Project Alpha</a>
<a className="nav-sublink" href="#" onClick={(e) => e.preventDefault()}>Project Beta</a>
<a className="nav-sublink" href="#" onClick={(e) => e.preventDefault()}>Project Gamma</a>
</ReactSidebarNavSubmenu>
</ReactSidebarNavItem>
<ReactSidebarNavItem>
<button
type="button"
className="nav-button"
>
<UserIcon />
<span className="nav-label">Team</span>
</button>
</ReactSidebarNavItem>
<ReactSidebarNavItem>
<button
type="button"
className="nav-button nav-button-expanded"
aria-expanded="false"
>
<SettingsIcon />
<span className="nav-label">Settings</span>
<span className="chevron"><ChevronRightIcon /></span>
</button>
{/* Popover for COLLAPSED mode */}
<ReactPopover
className="nav-button-collapsed"
placement="right-start"
triggerType="click"
distance={8}
arrow={true}
>
<button
slot="trigger"
type="button"
className="nav-button"
>
<SettingsIcon />
<span className="nav-label">Settings</span>
<span className="collapsed-indicator">
<svg viewBox="0 0 8 8" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M2 3l2 2 2-2" stroke="currentColor" strokeWidth="1" strokeLinecap="round"/>
</svg>
</span>
</button>
<ReactSidebarNavPopoverSubmenu
slot="content"
className="popover-submenu"
>
<a href="#" className="nav-sublink" onClick={(e) => e.preventDefault()}>Profile</a>
<a href="#" className="nav-sublink" onClick={(e) => e.preventDefault()}>Billing</a>
<a href="#" className="nav-sublink" onClick={(e) => e.preventDefault()}>Security</a>
<a href="#" className="nav-sublink" onClick={(e) => e.preventDefault()}>Preferences</a>
</ReactSidebarNavPopoverSubmenu>
</ReactPopover>
<ReactSidebarNavSubmenu>
<a className="nav-sublink" href="#" onClick={(e) => e.preventDefault()}>Profile</a>
<a className="nav-sublink" href="#" onClick={(e) => e.preventDefault()}>Billing</a>
<a className="nav-sublink" href="#" onClick={(e) => e.preventDefault()}>Security</a>
<a className="nav-sublink" href="#" onClick={(e) => e.preventDefault()}>Preferences</a>
</ReactSidebarNavSubmenu>
</ReactSidebarNavItem>
</ReactSidebarNav>
<div
slot="ag-footer"
style={{ fontSize: '0.875rem', color: 'var(--ag-text-secondary)' }}
>
© 2024 Company
</div>
</ReactSidebar>
<main style={{ flex: 1, padding: '2rem', overflow: 'auto', background: 'var(--ag-background)' }}>
<h1 style={{ marginTop: 0 }}>Main Content</h1>
<p>Click the header toggle to collapse the sidebar into rail mode.</p>
<p>When collapsed, hover over items with submenus to see them in popovers.</p>
</main>
</div>
</div>
<div className="mbe4">
<h2>Sidebar with Header Actions</h2>
<p>
Demonstrates using <code>ag-header-start</code>, <code>ag-header-end</code>, and <code>ag-header-toggle</code> slots
for a composable header layout with action buttons.
</p>
</div>
<div className="mbe6">
<div style={{ position: 'relative', display: 'flex', height: '500px', border: '1px solid var(--ag-border-color)', borderRadius: '0.5rem', overflow: 'hidden', contain: 'layout' }}>
<ReactSidebar
open={sidebar2.isOpen}
collapsed={sidebar2.isCollapsed}
showMobileToggle={true}
onToggle={(open) => setSidebar2({ ...sidebar2, isOpen: open })}
onCollapse={(collapsed) => setSidebar2({ ...sidebar2, isCollapsed: collapsed })}
>
<h2
slot="ag-header-start"
style={{ margin: 0, fontSize: '1.125rem', fontWeight: 600 }}
>
My Application
</h2>
<button
type="button"
slot="ag-header-end"
onClick={() => {}}
style={{ background: 'none', border: 'none', padding: '8px', cursor: 'pointer', display: 'flex', alignItems: 'center', color: 'inherit', borderRadius: '0.25rem' }}
aria-label="Settings"
title="Settings"
>
<SettingsIcon />
</button>
<button
type="button"
slot="ag-header-toggle"
onClick={toggleSidebar2Collapse}
style={{ background: 'none', border: 'none', padding: '8px 0', cursor: 'pointer', display: 'flex', alignItems: 'center', color: 'inherit' }}
aria-label="Toggle sidebar"
>
<PanelIcon />
</button>
<ReactSidebarNav>
<ReactSidebarNavItem>
<button
type="button"
className="nav-button active"
aria-current="page"
>
<HomeIcon />
<span className="nav-label">Dashboard</span>
</button>
</ReactSidebarNavItem>
<ReactSidebarNavItem>
<button
type="button"
className="nav-button"
>
<FolderIcon />
<span className="nav-label">Projects</span>
</button>
</ReactSidebarNavItem>
<ReactSidebarNavItem>
<button
type="button"
className="nav-button"
>
<UserIcon />
<span className="nav-label">Team</span>
</button>
</ReactSidebarNavItem>
</ReactSidebarNav>
<div
slot="ag-footer"
style={{ fontSize: '0.875rem', color: 'var(--ag-text-secondary)' }}
>
© 2024 Company
</div>
</ReactSidebar>
<main style={{ flex: 1, padding: '2rem', overflow: 'auto', background: 'var(--ag-background)' }}>
<h1 style={{ marginTop: 0 }}>Header Actions Pattern</h1>
<p>The header includes a settings button in the <code>ag-header-end</code> slot.</p>
<p>This allows for flexible header layouts with multiple action buttons.</p>
</main>
</div>
</div>
<div className="mbe4">
<h2>Sidebar with Built-in Toggle</h2>
<p>
Using <code>show-header-toggle</code> adds a built-in collapse button automatically.
</p>
</div>
<div className="mbe6">
<div style={{ position: 'relative', display: 'flex', height: '500px', border: '1px solid var(--ag-border-color)', borderRadius: '0.5rem', overflow: 'hidden', contain: 'layout' }}>
<ReactSidebar
open={sidebar3.isOpen}
collapsed={sidebar3.isCollapsed}
showHeaderToggle={true}
showMobileToggle={true}
onToggle={(open) => setSidebar3({ ...sidebar3, isOpen: open })}
onCollapse={(collapsed) => setSidebar3({ ...sidebar3, isCollapsed: collapsed })}
>
<h2
slot="ag-header-start"
style={{ margin: 0, fontSize: '1.125rem', fontWeight: 600 }}
>
Built-in Toggle
</h2>
<ReactSidebarNav>
<ReactSidebarNavItem>
<button
type="button"
className="nav-button active"
aria-current="page"
>
<HomeIcon />
<span className="nav-label">Dashboard</span>
</button>
</ReactSidebarNavItem>
<ReactSidebarNavItem>
<button
type="button"
className="nav-button"
>
<FolderIcon />
<span className="nav-label">Projects</span>
</button>
</ReactSidebarNavItem>
<ReactSidebarNavItem>
<button
type="button"
className="nav-button"
>
<UserIcon />
<span className="nav-label">Team</span>
</button>
</ReactSidebarNavItem>
</ReactSidebarNav>
<div
slot="ag-footer"
style={{ fontSize: '0.875rem', color: 'var(--ag-text-secondary)' }}
>
© 2024 Company
</div>
</ReactSidebar>
<main style={{ flex: 1, padding: '2rem', overflow: 'auto', background: 'var(--ag-background)' }}>
<h1 style={{ marginTop: 0 }}>Built-in Header Toggle</h1>
<p>No need to provide a custom toggle button—the sidebar includes one automatically.</p>
</main>
</div>
</div>
<div className="mbe4">
<h2>Disable Compact Mode</h2>
<p>
With <code>disable-compact-mode</code>, the sidebar has no intermediate collapsed/rail state.
It's either fully open (expanded) or completely hidden. This pattern is used in applications like Claude AI Studio.
</p>
</div>
<div className="mbe6">
<div style={{ position: 'relative', display: 'flex', height: '500px', border: '1px solid var(--ag-border-color)', borderRadius: '0.5rem', overflow: 'hidden', contain: 'layout' }}>
<ReactSidebar
open={sidebar4.isOpen}
disableCompactMode={true}
showMobileToggle={true}
onToggle={(open) => setSidebar4({ ...sidebar4, isOpen: open })}
>
<h2
slot="ag-header-start"
style={{ margin: 0, fontSize: '1.125rem', fontWeight: 600 }}
>
AI Studio
</h2>
<button
type="button"
slot="ag-header-toggle"
onClick={toggleSidebar4Responsive}
style={{ background: 'none', border: 'none', padding: '8px 0', cursor: 'pointer', display: 'flex', alignItems: 'center', color: 'inherit' }}
aria-label="Toggle sidebar"
>
<CommandIcon />
</button>
<ReactSidebarNav>
<ReactSidebarNavItem>
<button
type="button"
className="nav-button active"
aria-current="page"
>
<HomeIcon />
<span className="nav-label">Dashboard</span>
</button>
</ReactSidebarNavItem>
<ReactSidebarNavItem>
<button
type="button"
className="nav-button"
>
<FolderIcon />
<span className="nav-label">Projects</span>
</button>
</ReactSidebarNavItem>
<ReactSidebarNavItem>
<button
type="button"
className="nav-button"
>
<UserIcon />
<span className="nav-label">Team</span>
</button>
</ReactSidebarNavItem>
<ReactSidebarNavItem>
<button
type="button"
className="nav-button"
>
<SettingsIcon />
<span className="nav-label">Settings</span>
</button>
</ReactSidebarNavItem>
</ReactSidebarNav>
<div
slot="ag-footer"
style={{ fontSize: '0.875rem', color: 'var(--ag-text-secondary)' }}
>
© 2024 Company
</div>
</ReactSidebar>
<main style={{ flex: 1, padding: '2rem', overflow: 'auto', background: 'var(--ag-background)' }}>
<h1 style={{ marginTop: 0 }}>Disable Compact Mode</h1>
<p>Use the mobile toggle button to show/hide the sidebar.</p>
<p>Notice there's no collapsed/rail mode—it's either fully visible or completely hidden.</p>
</main>
</div>
</div>
<div className="mbe4">
<h2>Active Item Tracking</h2>
<p>
Click navigation items to see the active state change. This demonstrates how to track the current route
and apply active styling to both top-level and submenu items.
</p>
</div>
<div className="mbe6">
<div style={{ position: 'relative', display: 'flex', height: '500px', border: '1px solid var(--ag-border-color)', borderRadius: '0.5rem', overflow: 'hidden', contain: 'layout' }}>
<ReactSidebar
open={sidebar5.isOpen}
collapsed={sidebar5.isCollapsed}
showMobileToggle={true}
onToggle={(open) => setSidebar5({ ...sidebar5, isOpen: open })}
onCollapse={(collapsed) => setSidebar5({ ...sidebar5, isCollapsed: collapsed })}
>
<h2
slot="ag-header-start"
style={{ margin: 0, fontSize: '1.125rem', fontWeight: 600 }}
>
Navigation
</h2>
<button
type="button"
slot="ag-header-toggle"
onClick={toggleSidebar5Collapse}
style={{ background: 'none', border: 'none', padding: '8px 0', cursor: 'pointer', display: 'flex', alignItems: 'center', color: 'inherit' }}
aria-label="Toggle sidebar"
>
<PanelIcon />
</button>
<ReactSidebarNav>
<ReactSidebarNavItem>
<button
type="button"
className="nav-button active"
aria-current="page"
data-route="/dashboard"
onClick={(e) => handleNavClick('/dashboard', e)}
>
<HomeIcon />
<span className="nav-label">Dashboard</span>
</button>
</ReactSidebarNavItem>
<ReactSidebarNavItem>
<button
type="button"
className="nav-button"
data-route="/projects"
onClick={(e) => handleNavClick('/projects', e)}
>
<FolderIcon />
<span className="nav-label">Projects</span>
</button>
</ReactSidebarNavItem>
<ReactSidebarNavItem>
<button
type="button"
className="nav-button"
data-route="/team"
onClick={(e) => handleNavClick('/team', e)}
>
<UserIcon />
<span className="nav-label">Team</span>
</button>
</ReactSidebarNavItem>
<ReactSidebarNavItem>
<button
type="button"
className="nav-button nav-button-expanded"
aria-expanded="false"
data-route="/settings"
onClick={handleSettingsClick}
>
<SettingsIcon />
<span className="nav-label">Settings</span>
<span className="chevron"><ChevronRightIcon /></span>
</button>
<ReactPopover
className="nav-button-collapsed"
placement="right-start"
triggerType="click"
distance={8}
arrow={true}
>
<button
slot="trigger"
type="button"
className="nav-button"
data-route="/settings"
onClick={(e) => handleNavClick('/settings', e)}
>
<SettingsIcon />
<span className="nav-label">Settings</span>
<span className="collapsed-indicator">
<svg viewBox="0 0 8 8" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M2 3l2 2 2-2" stroke="currentColor" strokeWidth="1" strokeLinecap="round"/>
</svg>
</span>
</button>
<ReactSidebarNavPopoverSubmenu
slot="content"
className="popover-submenu"
>
<a
href="#"
className="nav-sublink"
data-route="/settings/profile"
onClick={(e) => handleNavClick('/settings/profile', e)}
>Profile</a>
<a
href="#"
className="nav-sublink"
data-route="/settings/billing"
onClick={(e) => handleNavClick('/settings/billing', e)}
>Billing</a>
<a
href="#"
className="nav-sublink"
data-route="/settings/security"
onClick={(e) => handleNavClick('/settings/security', e)}
>Security</a>
</ReactSidebarNavPopoverSubmenu>
</ReactPopover>
<ReactSidebarNavSubmenu>
<a
className="nav-sublink"
href="#"
data-route="/settings/profile"
onClick={(e) => handleNavClick('/settings/profile', e)}
>Profile</a>
<a
className="nav-sublink"
href="#"
data-route="/settings/billing"
onClick={(e) => handleNavClick('/settings/billing', e)}
>Billing</a>
<a
className="nav-sublink"
href="#"
data-route="/settings/security"
onClick={(e) => handleNavClick('/settings/security', e)}
>Security</a>
</ReactSidebarNavSubmenu>
</ReactSidebarNavItem>
</ReactSidebarNav>
</ReactSidebar>
<main style={{ flex: 1, padding: '2rem', overflow: 'auto', background: 'var(--ag-background)' }}>
<h1 style={{ marginTop: 0 }}>Active Item Tracking</h1>
<p>Current route: <strong>{sidebar5.activeRoute}</strong></p>
<p>Click navigation items to see the active state change.</p>
<ul>
<li><strong>Active styling:</strong> Background color and font weight change</li>
<li><strong>ARIA current:</strong> The active item has aria-current="page" for accessibility</li>
<li><strong>Submenu support:</strong> Sublinks also track active state</li>
<li><strong>Popover sync:</strong> Active state works in both inline and popover modes</li>
</ul>
<p>
In a real application, you'd integrate this with your router (Vue Router, etc.) to automatically
update the active state based on the current route.
</p>
</main>
</div>
</div>
</section>
);
}
Usage
TIP
The framework examples below import AgnosticUI as an npm package. Alternatively, you can use the CLI for complete control, AI/LLM visibility, and full code ownership:
npx ag init --framework FRAMEWORK # react, vue, lit, svelte, etc.
npx ag add SidebarThe CLI copies source code directly into your project, giving you full visibility and control. After running npx ag add, you'll receive exact import instructions.
Vue
<template>
<div style="display: flex; height: 100vh;">
<VueSidebar
:open="isOpen"
:collapsed="isCollapsed"
:show-mobile-toggle="true"
@update:open="isOpen = $event"
@update:collapsed="isCollapsed = $event"
>
<h2
slot="ag-header-start"
style="margin: 0; font-size: 1.125rem; font-weight: 600;"
>
Dashboard
</h2>
<button
type="button"
slot="ag-header-toggle"
@click="toggleCollapse"
style="background: none; border: none; padding: 8px 0; cursor: pointer;"
aria-label="Toggle sidebar"
>
<PanelIcon />
</button>
<VueSidebarNav>
<VueSidebarNavItem>
<button type="button" class="nav-button active" aria-current="page">
<Home :size="20" />
<span class="nav-label">Dashboard</span>
</button>
</VueSidebarNavItem>
<VueSidebarNavItem>
<!-- Button for expanded mode with rotating chevron -->
<button
type="button"
class="nav-button nav-button-expanded"
aria-expanded="false"
@click="handleSubmenuToggle"
>
<Folder :size="20" />
<span class="nav-label">Projects</span>
<span class="chevron"><ChevronRight :size="16" /></span>
</button>
<!-- Popover for collapsed mode - shows when sidebar is collapsed -->
<VuePopover
class="nav-button-collapsed"
placement="right-start"
trigger-type="click"
:distance="8"
:arrow="true"
>
<button
slot="trigger"
type="button"
class="nav-button"
aria-expanded="false"
>
<Folder :size="20" />
<span class="nav-label">Projects</span>
<span class="collapsed-indicator">
<svg
viewBox="0 0 8 8"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M2 3l2 2 2-2"
stroke="currentColor"
stroke-width="1"
stroke-linecap="round"
/>
</svg>
</span>
</button>
<VueSidebarNavPopoverSubmenu
slot="content"
class="nav-popover-submenu"
>
<a href="#" class="nav-sublink">Project Alpha</a>
<a href="#" class="nav-sublink">Project Beta</a>
</VueSidebarNavPopoverSubmenu>
</VuePopover>
<!-- Inline submenu for expanded mode -->
<VueSidebarNavSubmenu>
<a class="nav-sublink" href="#">Project Alpha</a>
<a class="nav-sublink" href="#">Project Beta</a>
</VueSidebarNavSubmenu>
</VueSidebarNavItem>
</VueSidebarNav>
<div slot="ag-footer" style="font-size: 0.875rem; color: #6b7280;">
© 2024 Company
</div>
</VueSidebar>
<main style="flex: 1; padding: 2rem;">
<h1>Main Content</h1>
</main>
</div>
</template>
<script>
import VueSidebar from "agnosticui-core/sidebar/vue";
import {
VueSidebarNav,
VueSidebarNavItem,
VueSidebarNavSubmenu,
VueSidebarNavPopoverSubmenu,
} from "agnosticui-core/sidebar-nav/vue";
import { VuePopover } from "agnosticui-core/popover/vue";
import { Home, Folder, ChevronRight } from "lucide-vue-next";
export default {
components: {
VueSidebar,
VueSidebarNav,
VueSidebarNavItem,
VueSidebarNavSubmenu,
VueSidebarNavPopoverSubmenu,
VuePopover,
Home,
Folder,
ChevronRight,
},
data() {
return {
isOpen: true,
isCollapsed: false,
};
},
methods: {
toggleCollapse() {
// Access the sidebar element to call toggleCollapse
const sidebar = this.$el.querySelector("ag-sidebar");
if (sidebar && sidebar.toggleCollapse) {
sidebar.toggleCollapse();
}
},
handleSubmenuToggle(e) {
const button = e.currentTarget;
const navItem = button.closest("ag-sidebar-nav-item");
const submenu = navItem?.querySelector("ag-sidebar-nav-submenu");
if (!submenu) return;
const isExpanded = button.getAttribute("aria-expanded") === "true";
if (isExpanded) {
button.setAttribute("aria-expanded", "false");
submenu.removeAttribute("open");
} else {
button.setAttribute("aria-expanded", "true");
submenu.setAttribute("open", "");
}
},
},
};
</script>React
import { useState } from "react";
import { ReactSidebar } from "agnosticui-core/sidebar/react";
import {
ReactSidebarNav,
ReactSidebarNavItem,
ReactSidebarNavSubmenu,
ReactSidebarNavPopoverSubmenu,
} from "agnosticui-core/sidebar-nav/react";
import { ReactPopover } from "agnosticui-core/popover/react";
import { Home, Folder, ChevronRight } from "lucide-react";
export default function SidebarExample() {
const [isOpen, setIsOpen] = useState(true);
const [isCollapsed, setIsCollapsed] = useState(false);
const handleSubmenuToggle = (e) => {
const button = e.currentTarget;
const navItem = button.closest("ag-sidebar-nav-item");
const submenu = navItem?.querySelector("ag-sidebar-nav-submenu");
if (!submenu) return;
const isExpanded = button.getAttribute("aria-expanded") === "true";
if (isExpanded) {
button.setAttribute("aria-expanded", "false");
submenu.removeAttribute("open");
} else {
button.setAttribute("aria-expanded", "true");
submenu.setAttribute("open", "");
}
};
return (
<div style={{ display: "flex", height: "100vh" }}>
<ReactSidebar
open={isOpen}
collapsed={isCollapsed}
showMobileToggle={true}
onToggle={(open) => setIsOpen(open)}
onCollapse={(collapsed) => setIsCollapsed(collapsed)}
>
<h2
slot="ag-header-start"
style={{ margin: 0, fontSize: "1.125rem", fontWeight: 600 }}
>
Dashboard
</h2>
<ReactSidebarNav>
<ReactSidebarNavItem>
<button
type="button"
className="nav-button active"
aria-current="page"
>
<Home size={20} />
<span className="nav-label">Dashboard</span>
</button>
</ReactSidebarNavItem>
<ReactSidebarNavItem>
{/* Button for expanded mode with rotating chevron */}
<button
type="button"
className="nav-button nav-button-expanded"
aria-expanded="false"
onClick={handleSubmenuToggle}
>
<Folder size={20} />
<span className="nav-label">Projects</span>
<span className="chevron">
<ChevronRight size={16} />
</span>
</button>
{/* Popover for collapsed mode - shows when sidebar is collapsed */}
<ReactPopover
className="nav-button-collapsed"
placement="right-start"
triggerType="click"
distance={8}
arrow={true}
>
<button
slot="trigger"
type="button"
className="nav-button"
aria-expanded="false"
>
<Folder size={20} />
<span className="nav-label">Projects</span>
<span className="collapsed-indicator">
<svg
viewBox="0 0 8 8"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M2 3l2 2 2-2"
stroke="currentColor"
strokeWidth="1"
strokeLinecap="round"
/>
</svg>
</span>
</button>
<ReactSidebarNavPopoverSubmenu
slot="content"
className="nav-popover-submenu"
>
<a href="#project-alpha" className="nav-sublink">
Project Alpha
</a>
<a href="#project-beta" className="nav-sublink">
Project Beta
</a>
</ReactSidebarNavPopoverSubmenu>
</ReactPopover>
{/* Inline submenu for expanded mode */}
<ReactSidebarNavSubmenu>
<a className="nav-sublink" href="#">
Project Alpha
</a>
<a className="nav-sublink" href="#">
Project Beta
</a>
</ReactSidebarNavSubmenu>
</ReactSidebarNavItem>
</ReactSidebarNav>
<div
slot="ag-footer"
style={{ fontSize: "0.875rem", color: "var(--ag-text-secondary)" }}
>
© 2024 Company
</div>
</ReactSidebar>
<main style={{ flex: 1, padding: "2rem" }}>
<h1>Main Content</h1>
</main>
</div>
);
}Lit (Web Components)
import { LitElement, html, css } from "lit";
import { customElement } from "lit/decorators.js";
import "agnosticui-core/sidebar";
import "agnosticui-core/sidebar-nav";
import "agnosticui-core/popover";
@customElement("sidebar-example")
export class SidebarExample extends LitElement {
static styles = css`
:host {
display: block;
}
`;
firstUpdated() {
// Set up event listeners for submenu toggles in the shadow DOM
const buttons = this.shadowRoot?.querySelectorAll(".nav-button-expanded");
buttons?.forEach((button) => {
button.addEventListener("click", (e: Event) => {
const navItem = (e.currentTarget as HTMLElement).closest(
"ag-sidebar-nav-item"
);
const submenu = navItem?.querySelector("ag-sidebar-nav-submenu");
if (!submenu) return;
const isExpanded = button.getAttribute("aria-expanded") === "true";
if (isExpanded) {
button.setAttribute("aria-expanded", "false");
submenu.removeAttribute("open");
} else {
button.setAttribute("aria-expanded", "true");
submenu.setAttribute("open", "");
}
});
});
}
render() {
return html`
<div style="display: flex; height: 100vh;">
<ag-sidebar id="sidebar" show-mobile-toggle>
<h2
slot="ag-header-start"
style="margin: 0; font-size: 1.125rem; font-weight: 600;"
>
Dashboard
</h2>
<ag-sidebar-nav>
<ag-sidebar-nav-item>
<button
type="button"
class="nav-button active"
aria-current="page"
>
<svg><!-- Home icon --></svg>
<span class="nav-label">Dashboard</span>
</button>
</ag-sidebar-nav-item>
<ag-sidebar-nav-item>
<!-- Button for expanded mode with rotating chevron -->
<button
type="button"
class="nav-button nav-button-expanded"
aria-expanded="false"
>
<svg><!-- Folder icon --></svg>
<span class="nav-label">Projects</span>
<span class="chevron"><!-- ChevronRight icon --></span>
</button>
<!-- Popover for collapsed mode - shows when sidebar is collapsed -->
<ag-popover
class="nav-button-collapsed"
placement="right-start"
trigger-type="click"
distance="8"
arrow
>
<button
slot="trigger"
type="button"
class="nav-button"
aria-expanded="false"
>
<svg><!-- Folder icon --></svg>
<span class="nav-label">Projects</span>
<span class="collapsed-indicator">
<svg
viewBox="0 0 8 8"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M2 3l2 2 2-2"
stroke="currentColor"
stroke-width="1"
stroke-linecap="round"
/>
</svg>
</span>
</button>
<ag-sidebar-nav-popover-submenu
slot="content"
class="nav-popover-submenu"
>
<a class="nav-sublink" href="#">Project Alpha</a>
<a class="nav-sublink" href="#">Project Beta</a>
</ag-sidebar-nav-popover-submenu>
</ag-popover>
<!-- Inline submenu for expanded mode -->
<ag-sidebar-nav-submenu>
<a class="nav-sublink" href="#">Project Alpha</a>
<a class="nav-sublink" href="#">Project Beta</a>
</ag-sidebar-nav-submenu>
</ag-sidebar-nav-item>
</ag-sidebar-nav>
<div
slot="ag-footer"
style="font-size: 0.875rem; color: var(--ag-text-secondary);"
>
© 2024 Company
</div>
</ag-sidebar>
<main style="flex: 1; padding: 2rem;">
<h1>Main Content</h1>
</main>
</div>
`;
}
}Note: When using sidebar components within a custom element's shadow DOM, set up event listeners in the component's lifecycle (e.g., firstUpdated()) rather than using DOMContentLoaded, as document.querySelector() cannot access elements inside shadow DOM. Use this.shadowRoot.querySelector() instead.
Props
Sidebar Component
| Prop | Type | Default | Description |
|---|---|---|---|
open | boolean | false | Controls sidebar visibility on mobile |
collapsed | boolean | false | Controls collapsed/rail state (icon-only) on desktop |
position | 'left' | 'right' | 'left' | Position of the sidebar |
ariaLabel | string | 'Navigation' | ARIA label for the sidebar |
variant | 'default' | 'bordered' | 'elevated' | 'default' | Visual variant of the sidebar |
noTransition | boolean | false | Disable transitions |
width | string | - | Custom width for the sidebar (e.g., '300px') |
disableCompactMode | boolean | false | Disable compact/rail mode (sidebar is either full-width or hidden) |
showMobileToggle | boolean | false | Show the mobile toggle button |
mobileTogglePosition | 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right' | 'top-left' | Position of the mobile toggle button |
showHeaderToggle | boolean | false | Show the built-in header toggle button |
Navigation Components
The Sidebar uses companion components for navigation:
VueSidebarNav/ReactSidebarNav/ag-sidebar-nav- Container for navigation itemsVueSidebarNavItem/ReactSidebarNavItem/ag-sidebar-nav-item- Individual navigation item wrapperVueSidebarNavSubmenu/ReactSidebarNavSubmenu/ag-sidebar-nav-submenu- Inline submenu for expanded modeVueSidebarNavPopoverSubmenu/ReactSidebarNavPopoverSubmenu/ag-sidebar-nav-popover-submenu- Popover submenu for collapsed mode
Events
| Event | Payload | Description |
|---|---|---|
@update:open (Vue) / onToggle (React) / toggle (Lit) | boolean | Emitted when the sidebar open state changes |
@update:collapsed (Vue) / onCollapse (React) / collapse (Lit) | boolean | Emitted when the sidebar collapsed state changes |
Slots
Vue
- slot="ag-header-start": Left-aligned header content (logo or title)
- slot="ag-header-end": Right-aligned header content (actions, before toggle)
- slot="ag-header-toggle": Custom collapse toggle button (always rightmost)
- slot="ag-header": Monolithic header slot (legacy, for full custom header)
- Default slot: Main content area for navigation
- slot="ag-footer": Footer content
React
- slot="ag-header-start": Left-aligned header content
- slot="ag-header-end": Right-aligned header content
- slot="ag-header-toggle": Custom collapse toggle button
- slot="ag-header": Monolithic header slot (legacy)
- children: Main content area for navigation
- slot="ag-footer": Footer content
Lit
- slot="ag-header-start": Left-aligned header content
- slot="ag-header-end": Right-aligned header content
- slot="ag-header-toggle": Custom collapse toggle button
- slot="ag-header": Monolithic header slot (legacy)
- Default slot: Main content area for navigation
- slot="ag-footer": Footer content
Methods
| Method | Description |
|---|---|
toggleCollapse() | Toggles the collapsed state of the sidebar |
toggleResponsive() | Toggles the responsive open/close state (used with disableCompactMode) |
Accessibility
The Sidebar component follows accessibility best practices:
- Uses semantic HTML with proper ARIA attributes
- Implements
role="navigation"with customizablearia-label - Supports keyboard navigation with Tab and Shift+Tab
- Active navigation items use
aria-current="page"for screen readers - Submenu buttons use
aria-expandedto indicate state - Focus management for smooth keyboard navigation
- Mobile overlay includes backdrop for visual separation
- Toggle buttons have proper
aria-labelattributes - Collapsed state indicators are handled with appropriate ARIA attributes
Best Practices
- Always provide an
ariaLabelto identify the sidebar navigation purpose - Use
aria-current="page"on the active navigation item - Ensure submenu buttons have
aria-expandedattribute - Provide clear visual indicators for active states
- Use the
.nav-labelclass on text content that should hide in collapsed mode - Structure navigation buttons with flexbox for proper icon/label layout
- Include both inline and popover versions of submenus for responsive behavior
- Test keyboard navigation flow through all items
- Ensure mobile toggle is easily accessible (consider thumb zones)
Use Cases
Expanded/Collapsed Mode (Default)
The sidebar can toggle between full-width (expanded) and icon-only (rail/collapsed) modes on desktop. On mobile, it becomes an overlay that can be opened/closed.
Use for:
- Dashboard applications with complex navigation
- Admin panels with multiple sections
- Documentation sites with nested navigation
Disable Compact Mode
With disableCompactMode, the sidebar has no intermediate collapsed state—it's either fully open or completely hidden. This matches patterns like Claude AI Studio.
Use for:
- Applications where icon-only mode isn't needed
- Simpler navigation structures
- Mobile-first applications
Header Patterns
Composable Header (ag-header-start, ag-header-end, ag-header-toggle):
- Logo/title on the left
- Actions (settings, notifications) in the middle
- Toggle button on the right
Monolithic Header (ag-header):
- Full control over header layout
- Logo-as-toggle pattern
- Custom header interactions
Built-in Toggle (show-header-toggle):
- Automatic toggle button
- No custom header slot needed
- Consistent behavior
Navigation Patterns
Simple Navigation: Single-level menu with icons and labels
Nested Navigation: Submenus that expand inline when sidebar is expanded, and show in popovers when collapsed
Active Item Tracking: Visual indication of current page with styling and aria-current
Styling Navigation
The Sidebar component provides the structure, but navigation button styling is typically handled in your application CSS (global light DOM styles). Here's the complete CSS required for a fully functional sidebar with all features:
/* CRITICAL: Sidebar component width - must be defined! */
ag-sidebar {
/* Expanded width */
width: 260px;
transition: width 0.3s ease;
overflow: visible; /* Allow popovers to show outside */
}
ag-sidebar[collapsed] {
/* Collapsed width - icon only */
width: 64px;
}
/* Navigation button base styles */
.nav-button {
display: flex;
align-items: center;
gap: var(--ag-space-3);
position: relative; /* Required for collapsed-indicator positioning */
padding: var(--ag-space-2) var(--ag-space-3);
border: none;
background: none;
cursor: pointer;
width: 100%;
text-align: left;
border-radius: var(--ag-radius-md);
transition: all var(--ag-fx-duration-sm);
color: var(--ag-text-primary);
font-size: var(--ag-font-size-base);
}
.nav-button:hover {
background: var(--ag-background-secondary);
}
.nav-button.active {
background: var(--ag-primary);
color: white;
font-weight: 500;
}
.nav-label {
flex-grow: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
transition: opacity var(--ag-fx-duration-sm);
}
/* Chevron rotation for expanded submenus */
.chevron {
display: flex;
align-items: center;
transition: transform var(--ag-fx-duration-md), opacity var(--ag-fx-duration-sm);
margin-left: auto;
}
.nav-button[aria-expanded="true"] .chevron {
transform: rotate(90deg);
}
/* Collapsed indicator - small triangle at 4:30 position */
.collapsed-indicator {
display: none;
position: absolute;
bottom: -1px;
right: -1px;
width: var(--ag-space-3);
height: var(--ag-space-3);
}
.collapsed-indicator svg {
color: var(--ag-text-secondary);
transform: rotate(315deg);
}
/* Show collapsed indicator in collapsed mode for buttons with submenus */
ag-sidebar[collapsed] .nav-button[aria-expanded] .collapsed-indicator {
display: block;
}
/* CRITICAL: Properly handle collapsed state */
ag-sidebar[collapsed] .nav-label,
ag-sidebar[collapsed] .chevron {
opacity: 0;
width: 0;
overflow: hidden;
}
/* Center icons in collapsed mode */
ag-sidebar[collapsed] .nav-button {
justify-content: center;
padding: var(--ag-space-2);
gap: 0;
}
/* Visibility rules for expanded vs collapsed mode */
ag-sidebar[collapsed] ag-sidebar-nav-submenu:not(.popover-submenu),
ag-sidebar:not([collapsed]) ag-popover,
ag-sidebar[collapsed] .nav-button-expanded,
ag-sidebar:not([collapsed]) .nav-button-collapsed {
display: none !important;
}
/* Fix popover centering in collapsed mode */
ag-sidebar[collapsed] ag-popover.nav-button-collapsed {
display: block !important;
}
/* CRITICAL: Submenu visibility - hidden by default, visible when open */
ag-sidebar-nav-submenu {
display: none;
overflow: hidden;
}
ag-sidebar-nav-submenu[open] {
display: block;
}
/* Submenu link styles */
.nav-sublink {
display: block;
padding: var(--ag-space-2) var(--ag-space-3);
padding-left: var(--ag-space-8);
color: var(--ag-text-secondary);
text-decoration: none;
border-radius: var(--ag-radius-md);
transition: background var(--ag-fx-duration-sm);
}
.nav-sublink:hover {
background: var(--ag-background-secondary);
color: var(--ag-text-primary);
}
/* Popover submenu styles */
.nav-popover-submenu .nav-sublink {
padding-left: var(--ag-space-3);
}
/* Popover content padding for nav popovers in collapsed mode */
.nav-button-collapsed::part(ag-popover-body) {
padding: var(--ag-space-1);
}
/* Hide popover header for nav popovers */
.nav-button-collapsed::part(ag-popover-header) {
display: none;
}Collapsed Indicator Markup
When using popovers for collapsed mode submenus, add the collapsed indicator inside the trigger button:
<button slot="trigger" type="button" class="nav-button" aria-expanded="false">
<FolderIcon />
<span class="nav-label">Projects</span>
<span class="collapsed-indicator">
<svg viewBox="0 0 8 8" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M2 3l2 2 2-2"
stroke="currentColor"
stroke-width="1"
stroke-linecap="round"
/>
</svg>
</span>
</button>This small triangular indicator appears at the bottom-right corner (4:30 clock position) when the sidebar is collapsed, helping users identify which navigation items have submenus.