Sidebar

A composable, collapsible sidebar that remembers whether it was open.

Workspace
  • 7
Inbox
Toggle with the button, the rail on the panel's edge, or B .

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.

Workspace
Collapse me
<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.