Skip to content
Home Theme Gallery

Sidebar

The custom element is <hui-sidebar>.

Overview

A collapsible application sidebar. The reader owns whether it is collapsed, the server persists and re-renders it, and the element never stores anything.

Example

Expanded with a rail and a footer
Source
<hui-sidebar state="expanded" collapsible="rail" side="left" label="Application" breakpoint="20rem">
<a class="hui-sidebar-brand" slot="brand" href="#top"><svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M12 2 3 7v10l9 5 9-5V7l-9-5Zm0 2.3 6.8 3.8L12 11.9 5.2 8.1 12 4.3Z"></path></svg><span class="hui-sidebar-brand-label">Home-UI</span></a>
<ul class="hui-sidebar-menu">
<li><a class="hui-sidebar-item" href="#dashboard" aria-current="page"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="3" y="3" width="7" height="7" rx="1"></rect><rect x="14" y="3" width="7" height="7" rx="1"></rect><rect x="3" y="14" width="7" height="7" rx="1"></rect><rect x="14" y="14" width="7" height="7" rx="1"></rect></svg><span class="hui-sidebar-item-label">Dashboard</span></a></li>
<li><a class="hui-sidebar-item" href="#entries"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M8 6h13M8 12h13M8 18h13"></path><circle cx="4" cy="6" r="1"></circle><circle cx="4" cy="12" r="1"></circle><circle cx="4" cy="18" r="1"></circle></svg><span class="hui-sidebar-item-label">Entries</span><span class="hui-sidebar-badge">12</span></a></li>
<li><a class="hui-sidebar-item" href="#settings"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4 6h16M4 12h16M4 18h16"></path><circle cx="9" cy="6" r="2"></circle><circle cx="15" cy="12" r="2"></circle><circle cx="7" cy="18" r="2"></circle></svg><span class="hui-sidebar-item-label">Settings</span></a></li>
</ul>
<div slot="footer">
<a class="hui-sidebar-item" href="#account"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="8" r="4"></circle><path d="M4 21v-1a8 8 0 0 1 16 0v1"></path></svg><span class="hui-sidebar-item-label">Account</span></a>
<a class="hui-sidebar-item" href="#help"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="9"></circle><path d="M9.5 9a2.5 2.5 0 1 1 3.5 2.3c-.7.4-1 .9-1 1.7"></path><circle cx="12" cy="17" r="1"></circle></svg><span class="hui-sidebar-item-label">Help</span></a>
</div>
</hui-sidebar>

The Go template that renders it:

<hui-sidebar state="{{ .SidebarState }}" collapsible="rail" side="left" label="Application">
<a class="hui-sidebar-brand" slot="brand" href="/">{{ .Logo }}<span class="hui-sidebar-brand-label">{{ .Product }}</span></a>
<ul class="hui-sidebar-menu">
{{ range .Nav }}
<li><a class="hui-sidebar-item" href="{{ .Href }}"{{ if .Current }} aria-current="page"{{ end }}>
{{ .Icon }}<span class="hui-sidebar-item-label">{{ .Label }}</span>
{{ if .Badge }}<span class="hui-sidebar-badge">{{ .Badge }}</span>{{ end }}
</a></li>
{{ end }}
</ul>
<div slot="footer">
{{ range .Footer }}
<a class="hui-sidebar-item" href="{{ .Href }}">{{ .Icon }}<span class="hui-sidebar-item-label">{{ .Label }}</span></a>
{{ end }}
</div>
</hui-sidebar>

On a phone

Below breakpoint the sidebar is a sheet, and the sheet starts closed whatever state says: state is the desktop layout the server persisted, and a phone should not open a modal over the page on every load. A control outside the sidebar opens it with toggle(); opening and closing announce hui-sidebar-sheet-toggle and leave state alone. Escape, a tap on the backdrop and the sheet’s own control close it.

Below the breakpoint: a sheet that starts closed 375 px wide
Source
<div style="min-height:420px">
<header style="display:flex;align-items:center;gap:.5rem;padding:.75rem 1rem;border-bottom:1px solid var(--hui-border)">
<hui-button variant="ghost" size="icon" aria-label="Open navigation" onclick="document.querySelector('hui-sidebar').toggle()"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M8 6h13M8 12h13M8 18h13"></path><circle cx="4" cy="6" r="1"></circle><circle cx="4" cy="12" r="1"></circle><circle cx="4" cy="18" r="1"></circle></svg></hui-button>
<strong style="font-size:.875rem">Dashboard</strong>
</header>
<hui-sidebar state="expanded" collapsible="rail" label="Application">
<div slot="header"><strong style="font-size:.875rem">Home-UI</strong></div>
<ul class="hui-sidebar-menu">
<li><a class="hui-sidebar-item" href="#dashboard" aria-current="page"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="3" y="3" width="7" height="7" rx="1"></rect><rect x="14" y="3" width="7" height="7" rx="1"></rect><rect x="3" y="14" width="7" height="7" rx="1"></rect><rect x="14" y="14" width="7" height="7" rx="1"></rect></svg><span class="hui-sidebar-item-label">Dashboard</span></a></li>
<li><a class="hui-sidebar-item" href="#entries"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M8 6h13M8 12h13M8 18h13"></path><circle cx="4" cy="6" r="1"></circle><circle cx="4" cy="12" r="1"></circle><circle cx="4" cy="18" r="1"></circle></svg><span class="hui-sidebar-item-label">Entries</span><span class="hui-sidebar-badge">12</span></a></li>
<li><a class="hui-sidebar-item" href="#settings"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4 6h16M4 12h16M4 18h16"></path><circle cx="9" cy="6" r="2"></circle><circle cx="15" cy="12" r="2"></circle><circle cx="7" cy="18" r="2"></circle></svg><span class="hui-sidebar-item-label">Settings</span></a></li>
</ul>
</hui-sidebar>
<p style="padding:0 1rem;font-size:.875rem">The page, with the sheet closed although the server rendered <code>state="expanded"</code>. The button in the header opens it.</p>
</div>

API

Attributes6
Attributes
NameTypeDefaultDescription
state"expanded" | "collapsed""expanded"Reflected. The server renders whatever it last persisted; the reader toggles it. With collapsible="overlay" it reads as pinned and unpinned - a float is never written here.
collapsible"rail" | "overlay" | "offcanvas" | "none""rail"What collapsing does. rail keeps a narrow strip beside the page; overlay keeps the same strip but floats the expanded panel over the page until the reader pins it; offcanvas hides it entirely; none disables collapsing.
collapse-control"header" | "bottom" | "none""header"Where the built-in collapse control is drawn: beside the product name (header), as a whole-wide item at the foot of the panel (bottom), or not at all (none), for a host that places its own control elsewhere and calls toggle(). A slotted trigger suppresses the built-in control whatever this says.
side"left" | "right""left"Which edge it sits against.
breakpointa CSS length"48rem"Below this the sidebar presents as a sheet, closed until something calls toggle(). Measured against the width of the element’s container.
labelstring"Sidebar"The accessible name on the rendered <aside>.
Properties7
Properties
NameTypeDefaultDescription
stateSidebarState'expanded'Reflects to state.
collapsibleSidebarCollapsible'rail'Reflects to collapsible.
collapseControlSidebarCollapseControl'header'Reflects to collapse-control.
sideSidebarSide'left'Reflects to side.
breakpointstring'48rem'Reflects to breakpoint.
sheetbooleanfalseDerived from the measured space; reflects to data-sheet.
peekedbooleanfalseRead-only. Whether the unpinned panel is floating open. The element carries data-peek while it is, which is what the floating layout selects on.
Methods2
Methods
NameTypeDescription
toggle()voidToggles the state, sets the attribute first, then emits hui-sidebar-toggle. Below the breakpoint it opens or closes the sheet instead, emits hui-sidebar-sheet-toggle and leaves state alone. With collapsible="overlay" it pins and unpins.
peek(open?)voidFloats the unpinned panel open over the page, or closes it. Only does anything with collapsible="overlay", unpinned and above the breakpoint. Emits hui-sidebar-peek; never writes state.
Events3
Events
NameDescription
hui-sidebar-toggleThe reader toggled it, after state is set. Detail: { state }. The host persists it; the component does not.
hui-sidebar-sheet-toggleBelow the breakpoint, the sheet opened or closed. Detail: { open }. Not state to persist: the sheet starts closed on every load.
hui-sidebar-peekAn unpinned sidebar floated open or closed again. Detail: { open }. Not state to persist either: a peek is a glance at a panel the reader has already given the space up for.
Slots5
Slots
NameDescription
(default)The menu the host rendered.
brandA logo or lockup. Wrap the wordmark beside it in .hui-sidebar-brand-label to have the rail take that away. When the built-in control is in the header (collapse-control="header", the default) the brand fades away as the rail narrows and the control takes its place, centred; with the control at the foot or none, the mark stays in the rail.
headerProduct name, switcher. Hidden when collapsed to a rail.
footerAccount, settings. A footer the host does not fill is not drawn at all, so it adds no rule at the foot of the panel.
triggerA control that toggles it, if the host wants it inside.
CSS custom properties6
CSS custom properties
NameDefaultDescription
--hui-sidebar-width16remExpanded width.
--hui-sidebar-height100svhPanel height; set it when the sidebar is not the full-height frame.
--hui-sidebar-rail-width3remWidth when collapsed to a rail. The default is the width at which an item's own padding centres its icon.
--hui-sidebar-sheet-width20remMaximum width of the sheet.
--hui-sidebar-scrimoklch(0 0 0 / 0.5)The sheet backdrop.
--hui-sidebar-peek-shadow6px 0 20px -6px oklch(0 0 0 / 0.22)The shadow under a floating unpinned panel, cast sideways towards the page it covers. The default mirrors itself for side="right"; set it and you own the direction too.
Classes8
Classes
NameDescription
.hui-sidebar-menuThe menu list inside hui-sidebar.
<ul class="hui-sidebar-menu">
  <li><a class="hui-sidebar-item" href="/" aria-current="page">
    <span class="hui-sidebar-item-label">Dashboard</span>
  </a></li>
</ul>
.hui-sidebar-groupA labelled group of sidebar items.
.hui-sidebar-group-labelThe label above a sidebar group. Hidden when the sidebar is collapsed to a rail.
.hui-sidebar-itemA sidebar link. The current item is the host’s aria-current="page".
.hui-sidebar-item-labelThe text of a sidebar item. Wrap it so the collapsed rail can hide the label and show the icon alone.
.hui-sidebar-badgeA count badge pushed to the end of a sidebar item. Hidden when collapsed to a rail.
.hui-sidebar-brandA logo in the brand slot. The mark survives a collapse to a rail, which the header slot beside it does not. Its inline-start padding is the item’s, so the mark lines up with the menu icons below it.
<a class="hui-sidebar-brand" slot="brand" href="/">
  <svg width="20" height="20" aria-hidden="true">...</svg>
  <span class="hui-sidebar-brand-label">Acme</span>
</a>
.hui-sidebar-brand-labelThe wordmark beside the brand mark. Hidden when collapsed to a rail, so the mark is left on its own.
::part() hooks7
::part() hooks
NameDescription
sidebarThe panel.
toggleThe built-in collapse control, drawn beside the product name when collapse-control="header" (the default) and the host supplies no trigger.
toggle-bottomThe built-in collapse control when collapse-control="bottom": a full-width item at the foot of the panel, its label beside the icon. The rail hides the label and centres the icon, as it does for a menu item.
railThe collapse affordance and the strip that survives a collapse.
headerThe header block.
contentThe scrollable menu area.
footerThe footer block.

States

Collapsed to a rail

state="collapsed" with collapsible="rail": the panel narrows to a rail of icons, and a sidebar rendered in this state arrives collapsed without animating. The brand fades away as it collapses, since the header control takes its place; see collapse-control="bottom" below to keep the mark in the rail.

Collapsed to a rail
Source
<hui-sidebar state="collapsed" collapsible="rail" side="left" label="Application" breakpoint="20rem">
<a class="hui-sidebar-brand" slot="brand" href="#top"><svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M12 2 3 7v10l9 5 9-5V7l-9-5Zm0 2.3 6.8 3.8L12 11.9 5.2 8.1 12 4.3Z"></path></svg><span class="hui-sidebar-brand-label">Home-UI</span></a>
<ul class="hui-sidebar-menu">
<li><a class="hui-sidebar-item" href="#dashboard" aria-current="page"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="3" y="3" width="7" height="7" rx="1"></rect><rect x="14" y="3" width="7" height="7" rx="1"></rect><rect x="3" y="14" width="7" height="7" rx="1"></rect><rect x="14" y="14" width="7" height="7" rx="1"></rect></svg><span class="hui-sidebar-item-label">Dashboard</span></a></li>
<li><a class="hui-sidebar-item" href="#entries"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M8 6h13M8 12h13M8 18h13"></path><circle cx="4" cy="6" r="1"></circle><circle cx="4" cy="12" r="1"></circle><circle cx="4" cy="18" r="1"></circle></svg><span class="hui-sidebar-item-label">Entries</span></a></li>
<li><a class="hui-sidebar-item" href="#settings"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4 6h16M4 12h16M4 18h16"></path><circle cx="9" cy="6" r="2"></circle><circle cx="15" cy="12" r="2"></circle><circle cx="7" cy="18" r="2"></circle></svg><span class="hui-sidebar-item-label">Settings</span></a></li>
</ul>
<div slot="footer">
<a class="hui-sidebar-item" href="#account"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="8" r="4"></circle><path d="M4 21v-1a8 8 0 0 1 16 0v1"></path></svg><span class="hui-sidebar-item-label">Account</span></a>
<a class="hui-sidebar-item" href="#help"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="9"></circle><path d="M9.5 9a2.5 2.5 0 1 1 3.5 2.3c-.7.4-1 .9-1 1.7"></path><circle cx="12" cy="17" r="1"></circle></svg><span class="hui-sidebar-item-label">Help</span></a>
</div>
</hui-sidebar>

Unpinned: the panel floats over the page

collapsible="overlay" adds a third state to the two above. Unpinned, the rail keeps its place in the flow and the expanded panel is drawn over the page rather than beside it: hover the rail, focus into it, or click the strip at its edge, and it floats out; a moment after the pointer leaves, it closes again. The header control pins and unpins rather than expanding and collapsing, and pinning gives the page’s space back to an in-flow panel. Nothing about the float is persisted - state is only ever pinned or unpinned.

Unpinned: the panel floats over the page
Source
<div style="display:flex;min-height:380px">
<hui-sidebar state="collapsed" collapsible="overlay" side="left" label="Application" breakpoint="20rem">
<a class="hui-sidebar-brand" slot="brand" href="#top"><svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M12 2 3 7v10l9 5 9-5V7l-9-5Zm0 2.3 6.8 3.8L12 11.9 5.2 8.1 12 4.3Z"></path></svg><span class="hui-sidebar-brand-label">Home-UI</span></a>
<ul class="hui-sidebar-menu">
<li><a class="hui-sidebar-item" href="#dashboard" aria-current="page"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="3" y="3" width="7" height="7" rx="1"></rect><rect x="14" y="3" width="7" height="7" rx="1"></rect><rect x="3" y="14" width="7" height="7" rx="1"></rect><rect x="14" y="14" width="7" height="7" rx="1"></rect></svg><span class="hui-sidebar-item-label">Dashboard</span></a></li>
<li><a class="hui-sidebar-item" href="#entries"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M8 6h13M8 12h13M8 18h13"></path><circle cx="4" cy="6" r="1"></circle><circle cx="4" cy="12" r="1"></circle><circle cx="4" cy="18" r="1"></circle></svg><span class="hui-sidebar-item-label">Entries</span><span class="hui-sidebar-badge">12</span></a></li>
<li><a class="hui-sidebar-item" href="#settings"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4 6h16M4 12h16M4 18h16"></path><circle cx="9" cy="6" r="2"></circle><circle cx="15" cy="12" r="2"></circle><circle cx="7" cy="18" r="2"></circle></svg><span class="hui-sidebar-item-label">Settings</span></a></li>
</ul>
</hui-sidebar>
<div style="flex:1 1 auto;padding:1rem;font-size:.875rem">
<p style="margin:0 0 .5rem"><strong>The page</strong></p>
<p style="margin:0;color:var(--hui-muted-foreground)">Hover the rail. The panel is drawn over this paragraph rather than pushing it aside, and the page keeps its width for the whole of it. Pin it with the control in the header and the space is handed over properly.</p>
</div>
</div>

The footer slot carrying an account row and secondary links, with an avatar sized to the icons beside it.

Account and secondary actions in the footer
Source
<hui-sidebar state="expanded" collapsible="rail" side="left" label="Application" breakpoint="20rem">
<div slot="header"><strong style="font-size:.875rem">Home-UI</strong></div>
<ul class="hui-sidebar-menu">
<li><a class="hui-sidebar-item" href="#dashboard" aria-current="page"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="3" y="3" width="7" height="7" rx="1"></rect><rect x="14" y="3" width="7" height="7" rx="1"></rect><rect x="3" y="14" width="7" height="7" rx="1"></rect><rect x="14" y="14" width="7" height="7" rx="1"></rect></svg><span class="hui-sidebar-item-label">Dashboard</span></a></li>
<li><a class="hui-sidebar-item" href="#entries"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M8 6h13M8 12h13M8 18h13"></path><circle cx="4" cy="6" r="1"></circle><circle cx="4" cy="12" r="1"></circle><circle cx="4" cy="18" r="1"></circle></svg><span class="hui-sidebar-item-label">Entries</span></a></li>
</ul>
<div slot="footer">
<a class="hui-sidebar-item" href="#account">
<span class="hui-avatar"><span class="hui-avatar__fallback">AB</span></span>
<span class="hui-sidebar-item-label">Ada Bell</span>
</a>
<a class="hui-sidebar-item" href="#settings"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4 6h16M4 12h16M4 18h16"></path><circle cx="9" cy="6" r="2"></circle><circle cx="15" cy="12" r="2"></circle><circle cx="7" cy="18" r="2"></circle></svg><span class="hui-sidebar-item-label">Settings</span></a>
<a class="hui-sidebar-item" href="#signout"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"></path><path d="M16 17l5-5-5-5"></path><path d="M21 12H9"></path></svg><span class="hui-sidebar-item-label">Sign out</span></a>
</div>
</hui-sidebar>

Collapse control at the foot

collapse-control="bottom" draws the control as a whole-wide item at the foot of the panel instead of beside the product name - where an application usually puts it. collapse-control="none" draws no built-in control and drops the rail strip as well, for a host that places its own button and calls toggle(); a slotted trigger suppresses the built-in control whatever this says.

Collapse control at the foot
Source
<hui-sidebar state="expanded" collapsible="rail" collapse-control="bottom" side="left" label="Application" breakpoint="20rem">
<a class="hui-sidebar-brand" slot="brand" href="#top"><svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M12 2 3 7v10l9 5 9-5V7l-9-5Zm0 2.3 6.8 3.8L12 11.9 5.2 8.1 12 4.3Z"></path></svg><span class="hui-sidebar-brand-label">Home-UI</span></a>
<ul class="hui-sidebar-menu">
<li><a class="hui-sidebar-item" href="#dashboard" aria-current="page"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="3" y="3" width="7" height="7" rx="1"></rect><rect x="14" y="3" width="7" height="7" rx="1"></rect><rect x="3" y="14" width="7" height="7" rx="1"></rect><rect x="14" y="14" width="7" height="7" rx="1"></rect></svg><span class="hui-sidebar-item-label">Dashboard</span></a></li>
<li><a class="hui-sidebar-item" href="#entries"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M8 6h13M8 12h13M8 18h13"></path><circle cx="4" cy="6" r="1"></circle><circle cx="4" cy="12" r="1"></circle><circle cx="4" cy="18" r="1"></circle></svg><span class="hui-sidebar-item-label">Entries</span></a></li>
<li><a class="hui-sidebar-item" href="#settings"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4 6h16M4 12h16M4 18h16"></path><circle cx="9" cy="6" r="2"></circle><circle cx="15" cy="12" r="2"></circle><circle cx="7" cy="18" r="2"></circle></svg><span class="hui-sidebar-item-label">Settings</span></a></li>
</ul>
</hui-sidebar>

Server-side mechanics

The server owns the menu contents and which item is current (aria-current="page"), and it renders the state it last persisted. The sidebar is not form-associated and submits nothing. The reader’s toggle emits hui-sidebar-toggle; the host persists it however it already persists things and re-renders the attribute on the next request. An hx-swap that replaces the sidebar re-reads state; a collapse the reader made in the meantime is discarded.

Accessibility

  • The panel is a named <aside> landmark in the wide layout.
  • Below the breakpoint, and open, it becomes a role="dialog" with aria-modal, and focus is trapped with the shared helper and restored on close.
  • The rail carries aria-expanded, so the collapsed state is announced.
  • Where a peek is possible the header control is announced as a pressed state rather than an expanded one, because what it changes is whether the panel is pinned - the panel is showing either way.
  • The bottom control is the same button as the header one: the same accessible name, and the same expanded or pressed state, whichever place collapse-control puts it.
  • A float is reachable without a pointer: focus opens it, Escape closes it, and the rail strip is a real button that toggles it.

Keyboard

Keyboard
KeysAction
Enter, SpaceToggles from the rail or a slotted trigger.
TabMoves through the menu items; the rail is a real control. Tabbing into an unpinned sidebar floats it open at once, with no hover dwell, and tabbing out of it closes it again.
EscapeCloses the sheet when it presents as one below the breakpoint, and closes a floating unpinned panel. It does not unpin: a pin is a layout, not an overlay.

Gotchas

  • The element writes nothing to storage - no cookie, no localStorage. Persisting the state is the host’s job; the event is the hook.
  • The breakpoint is measured against the space the sidebar sits in (its parent), not the viewport, so a sidebar inside a narrow panel presents as a sheet there.
  • For the collapsed rail to show icons alone, wrap each item’s text in <span class="hui-sidebar-item-label">. The sheet hides that span, the badge and the group label when collapsed; a bare text node cannot be hidden and is clipped instead.
  • It is 100svh tall by default, because a sidebar is usually the application frame. Set --hui-sidebar-height when it is not.
  • A floating panel draws over the page, so the page needs somewhere to be drawn under. Put the sidebar and the page in the same row - the element keeps the rail’s width in the flow for the whole float, so the page never moves.
  • overlay is the only value that floats. It and rail collapse to the same strip, so switching between them changes nothing about the collapsed state, only what expanding from it does.
  • A peek closes on the pointer leaving, not on a click inside it: an item is still a link, and following it is what closes the sidebar, by navigating.
  • collapse-control="none" removes the rail strip too, not only the header control, so the element carries no built-in affordance at all; wire a host button to toggle(). Use a slotted trigger instead when the host wants its own control drawn in the header beside the product name.
  • With the header control (the default) the brand fades out as the sidebar collapses and is gone in the rail, so the control stands alone and centred; the brand and the control do not stack. Put the control at the foot (collapse-control="bottom") when the mark should stay in the rail.