Sidebar
A composable, collapsible sidebar that remembers whether it was open.
Installation
uvx django_shadcn@latest add sidebar
Usage
<c-sidebar.provider>
<c-sidebar>
<c-sidebar.header>...</c-sidebar.header>
<c-sidebar.content>
<c-sidebar.group>
<c-sidebar.group-label>Workspace</c-sidebar.group-label>
<c-sidebar.group-content>
<c-sidebar.menu>
<c-sidebar.menu-item>
<c-sidebar.menu-button href="/inbox/" tooltip="Inbox">
<c-icon name="search" />
<span>Inbox</span>
</c-sidebar.menu-button>
</c-sidebar.menu-item>
</c-sidebar.menu>
</c-sidebar.group-content>
</c-sidebar.group>
</c-sidebar.content>
<c-sidebar.footer>...</c-sidebar.footer>
<c-sidebar.rail />
</c-sidebar>
<c-sidebar.inset>
<c-sidebar.trigger />
{{ content }}
</c-sidebar.inset>
</c-sidebar.provider>
<c-sidebar.provider> holds the open state and has to wrap both the panel and
the page. Put it in your base template, around everything.
<c-sidebar.inset> is where the page goes. It must be a sibling that comes
after <c-sidebar> — the inset variant styles it from there.
A <c-sidebar.menu-button> with an href renders an <a>; without one it
renders a <button>. Mark the current page with is_active="true".
Remembering the state
Toggling writes a sidebar_state cookie. Read it back in the view so the
server renders the panel in the shape the visitor left it, instead of drawing
it expanded and letting it snap shut once Alpine boots:
def dashboard(request):
return render(request, "dashboard.html", {
"sidebar_open": request.COOKIES.get("sidebar_state", "true"),
})
<c-sidebar.provider default_open="{{ sidebar_open }}">
Examples
Collapse to icons
collapsible="icon" keeps a rail of icons instead of sliding the whole panel
away. Labels, badges and sub-menus hide themselves; the tooltip on each
button takes over as the label.
<c-sidebar collapsible="icon">...</c-sidebar>
Variants and side
<c-sidebar variant="floating">...</c-sidebar>
<c-sidebar variant="inset">...</c-sidebar>
<c-sidebar side="right">...</c-sidebar>
<c-sidebar collapsible="none">...</c-sidebar>
floating detaches the panel with a border and a radius. inset does the same
to the page instead, and needs the provider to sit on the sidebar background —
has-data-[variant=inset]:bg-sidebar already handles that. none drops the
toggle entirely and leaves a panel that is always there.
Nested items
<c-sidebar.menu-item>
<c-sidebar.menu-button>
<span>Settings</span>
</c-sidebar.menu-button>
<c-sidebar.menu-sub>
<c-sidebar.menu-sub-item>
<c-sidebar.menu-sub-button href="/settings/team/" is_active="true">
<span>Team</span>
</c-sidebar.menu-sub-button>
</c-sidebar.menu-sub-item>
</c-sidebar.menu-sub>
</c-sidebar.menu-item>
Wrap the sub-menu in a Collapsible if you want it to fold.
Actions and badges
<c-sidebar.menu-item>
<c-sidebar.menu-button>
<span>Done</span>
</c-sidebar.menu-button>
<c-sidebar.menu-action show_on_hover="true">
<c-icon name="ellipsis" />
</c-sidebar.menu-action>
</c-sidebar.menu-item>
Both the action and the badge sit on top of the button, so they go after it,
inside the same <c-sidebar.menu-item>. Only one of the two per item — they
claim the same corner.
Loading placeholder
<c-sidebar.menu-skeleton show_icon="true" width="60%" />
Notes
Below md the panel slides in over a backdrop rather than becoming a
Sheet as upstream does. One tree serves both
widths, so the menu is written once and there are no duplicate ids in the page.
The width comes from --sidebar-width and --sidebar-width-icon, set on the
provider. Override them with an inline style, which beats the defaults:
<c-sidebar.provider style="--sidebar-width: 20rem">
<c-sidebar.menu-button> renders its tooltip as a plain sibling label
instead of composing Tooltip, because the
badge and action rules position themselves against the button as a sibling and
a wrapper would break them.
The sidebar's own colour tokens — --sidebar, --sidebar-accent,
--sidebar-border and the rest — live in input.css. Restyle the panel there,
not with utility classes on each part.