Skip to content
Glint UI

Sidebar

The app's navigation column — a shell that owns its width and collapse, and the parts you stack inside it.

Usage

import { Sidebar, SidebarHeader, SidebarContent, SidebarFooter, SidebarMenu } from "@glint/components/sidebar.slint";
import { Button, ButtonVariant } from "@glint/components/button.slint";
import { Tokens } from "@glint/theme/tokens.slint";
import { IconSet } from "@lucide";
export component AppWindow inherits Window {
width: 720px;
height: 400px;
background: Tokens.color-background;
property <string> route: "dashboard";
bar := Sidebar {
x: 0;
y: 0;
height: parent.height;
accessible-label: "Main";
SidebarHeader {
Button { variant: ButtonVariant.ghost; text: "Acme Inc"; }
}
SidebarContent {
SidebarMenu {
heading: "Platform";
// The parts are told about the collapse: a slotted component
// cannot read its host.
collapsed: bar.collapsed;
active: root.route;
items: [
{ label: "Dashboard", id: "dashboard", icon: IconSet.LayoutDashboard },
{ label: "Reports", id: "reports", icon: IconSet.ChartLine },
];
navigate(id) => { root.route = id; }
}
}
SidebarFooter {
Button { variant: ButtonVariant.ghost; text: "Account"; }
}
}
// The page, beside the column. The sidebar owns its own width, so the
// content area is measured from it.
Rectangle {
x: bar.width;
width: parent.width - bar.width;
height: parent.height;
}
}

Sidebar owns the column itself — its chrome, its width, the collapse animation, side, variant and collapsible, and the edge that resizes and toggles it. It spends its one @children slot on the column’s contents, so a workspace switcher and a user menu are whatever they need to be rather than whatever a struct field could hold (ADR-0029).

Three parts go in that slot, and the menus go inside the middle one:

PartWhere it sits
SidebarHeaderIn the column’s slot — the sticky top, a workspace switcher, a brand
SidebarContentIn the column’s slot — the scrolling middle, taking the room the two ends leave
SidebarFooterIn the column’s slot — the sticky bottom, a user menu, a settings row
SidebarMenuInside SidebarContent: one section of rows with an optional heading. Several of them are what makes a column read as sections

The parts that react to the collapse are told about it at the call site (collapsed: bar.collapsed), because Slint lets neither a parent address its slotted children nor a slotted component read its host (ADR-0021). That one line is the whole wiring.

The column places itself: it publishes its own width, which is what the page beside it is measured from. Putting a Sidebar inside a layout hands the width to the layout instead, and the collapse animation goes with it.

Examples

What collapsing leaves behind

collapsible decides what is left when the column shuts. SidebarCollapsible.icon — the default — leaves the icon-only rail: labels, badges, chevrons and trailing actions leave the layout, and the icons stay. offcanvas takes the column to nothing but the edge. none is a column that does not collapse at all, and so offers no edge to collapse it with.

collapsed is the state, and it is two-way — an app that remembers whether the column was shut restores it by writing that property. toggle-collapsed() is the flip: it moves collapsed and fires toggle(), which is what the edge and a header button both call so the two cannot drift apart.

Which side it stands on

side decides which of the column’s edges is the inner one — the edge the handle rides, and the direction the arrow keys widen it in. A right-hand column puts its handle on its left, and ← widens it.

How the column meets the page

variant is that meeting. SidebarVariant.sidebar is flush against the page, with a hairline down its inner edge. floating detaches it into a card with a gap around it and a shadow under it. inset draws no surface of its own — the column is a region of the page rather than a panel on it.

The edge

The edge is the column’s width made grabbable and its collapse made clickable. A press that travelled is a resize; a press that stayed put is the toggle — both live on the same handle, so the drag is what cancels the click.

min-column-width and max-column-width bound the drag, expanded-width is where it currently stands (two-way, so an app can restore a remembered width), and resized fires with the new width as it moves. From the keyboard, ← and → nudge it by keyboard-step, and Enter or Space collapses the column.

What a menu row can be

A SidebarItem is a row: a label, the id you compare active against, a lucide icon, a badge at the trailing edge, and an optional trailing action button named by action-label. Give it children and it stops navigating — it becomes a parent that expands, the way a menu row with children opens instead of firing (ADR-0020). expanded is written back into the model, so the model is the state.

heading-action-icon puts one more button beside the section’s heading — “add a project”.

API Reference

Properties

PropertyTypeDefaultDescription
collapsedin-out boolfalseTwo-way; when true the column shrinks to what collapsible leaves.
collapsiblein SidebarCollapsibleSidebarCollapsible.iconWhat collapsing leaves behind.
sidein SidebarSideSidebarSide.leftWhich side of the app the column stands on.
variantin SidebarVariantSidebarVariant.sidebarHow the column meets the page.
collapsed-widthin length56pxWidth of the icon-only rail.
expanded-widthin-out length240pxTwo-way; width while expanded. The edge drags it, within the bounds below.
min-column-widthin length180pxHow narrow and how wide a drag may take the column.
max-column-widthin length400px
keyboard-stepin length16pxHow far one arrow key, or one accessible increment, moves the edge.
edge-labelin string@tr("Resize sidebar")Name of the edge handle; override to translate.

Callbacks

CallbackDescription
toggle()Fired after the column collapses or expands.
resized(length)Fired with the column's new width while the edge is dragged.

Functions

FunctionDescription
toggle-collapsed()

Enums

EnumValues
SidebarCollapsibleoffcanvas, icon, none
SidebarSideleft, right
SidebarVariantsidebar, floating, inset

SidebarHeader

The sticky top of the column. It keeps its own height while the scrolling middle takes what is left.

SidebarHeader publishes no properties, callbacks or functions of its own. What a call site can set on it is the Slint Rectangle it inherits.

SidebarContent

The scrolling middle. It rides the library’s one scrolling surface (ADR-0019), so a menu longer than the column scrolls instead of overflowing it.

content-takes-focus defaults to true here, where ScrollArea’s own default is false: a column of SidebarMenu rows answers the keyboard itself, so the region needs no stop of its own. The body is @children though, so it stays the consumer’s fact — a column filled with inert rows and left at true is a scrolling region with no tab stop at all, which is content a keyboard user cannot read.

Properties

PropertyTypeDefaultDescription
content-takes-focusin booltrueWhether what is slotted in here answers the keyboard itself. A SidebarMenu does — every row of one is a focusable button — which is why this defaults to true where ADR-0032's own default is false — but the body is @children, so it is the consumer's fact and not this component's to settle. A column filled with inert rows and left at true is a scrolling region with no tab stop at all, which is content a keyboard user cannot read; DialogPanel and AttachmentGroup publish the property for the same reason.

SidebarFooter

The sticky bottom of the column.

SidebarFooter publishes no properties, callbacks or functions of its own. What a call site can set on it is the Slint Rectangle it inherits.

SidebarMenu

One section of the column: an optional heading, an optional action beside it, and the rows. A Separator between two sections is yours to place, since the content region is a slot.

Properties

PropertyTypeDefaultDescription
itemsin-out [SidebarItem]no defaultRows top-to-bottom. in-out because expanding a parent row writes its new expanded back here.
activein-out stringno defaultTwo-way; id of the active row. Compare against items[i].id.
collapsedin boolfalseTrue while the column is collapsed to its rail: labels, badges and actions leave the layout and only the icons remain.
headingin stringno defaultNames the section, above its rows. Empty draws no heading.
heading-action-iconin LucideIconno defaultAn action beside the heading — "add a project". An empty Icon draws none; the label names it, since the button carries no text.
heading-action-labelin stringno default

Callbacks

CallbackDescription
navigate(string)Fired with the chosen row's id; active updates first.
action(string)Fired with the row a trailing action sits on.
toggled(string , bool)Fired with a parent row's id and whether it is now open.
heading-action()Fired by the action beside the heading.

SidebarItem and SidebarSubItem are data types rather than components:

TypeFields
SidebarItemlabel, id, icon, badge, action-icon, action-label, children, expanded
SidebarSubItemlabel, id, icon, badge

Accessibility

  • The shell is a navigation landmark. Sidebar carries accessible-role: navigation and no name of its own, so a call site’s accessible-label lands on it. An app with two columns needs them told apart.
  • A menu is a list named by its heading. SidebarMenu publishes accessible-role: list with its heading as the label and its row count as the item count. A call site that wants other wording overrides accessible-label.
  • Every row is a named button carrying its index, whether it is selected, and — for a parent — whether it is expanded. The name lives on the row rather than on its text, so it survives the collapse that drops the label out of the layout: the icon-only rail still announces “Dashboard”.
  • A badge is content, not a node. The count rides as the row’s accessible-description, so the row speaks as a single control rather than as a control followed by a stray number.
  • A trailing action is its own control. It sits above the row’s touch area and takes the press before it, carries action-label as its name, and has its own tab stop and focus ring — a control inside a row is a control, not a part of the row (ADR-0028).
  • Sub-rows are a list of their own. A parent’s children are announced with their own count and their own indices — an unnamed list, so a sub-row is “2 of 3” rather than a second row 0 inside the section’s count (ADR-0012). While the parent is shut they are gone from the tree, not merely hidden — and the rail shows none of them, because it has no room for them.
  • The edge speaks as a slider. Slint 1.17 has no separator role (ADR-0015), and a slider is the role that carries a value, its bounds and the actions that move it: the edge publishes expanded-width as its value — the width the column returns to, not the width it currently draws — along with min-column-width, max-column-width and keyboard-step. It answers increment and decrement, accepts a value directly, and takes the collapse as its default action.