Skip to content
ShineUI
Install

SideNav

The app rail.

<sui-side-nav>Menus and navigationStable

About

Sections of navigation items for an application shell, on the brand chrome or on the page surface, with an app mark that survives the collapse to a 60px icon rail. Groups expand, or fly out beside the collapsed rail, badges count, and the accent marks the current page. Reach for it as the leading navigation rail of an app.

Usage

<sui-side-nav aria-label="Primary" height="360" active="Projects"> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" fill="none" slot="icon" aria-hidden="true" focusable="false" style="width:28px;height:28px;display:block"> <rect width="32" height="32" rx="9" fill="var(--accent)"/> <g fill="var(--accent-contrast)"> <path d="M12.5 6.5H23A2.5 2.5 0 0 1 25.5 9V10.5A2.5 2.5 0 0 1 23 13H15.5A2.5 2.5 0 0 0 13 15.5V23A2.5 2.5 0 0 1 10.5 25.5H9A2.5 2.5 0 0 1 6.5 23V12.5A6 6 0 0 1 12.5 6.5Z"/> <rect x="17.5" y="17.5" width="8" height="8" rx="3"/> </g> </svg> <b slot="brand" style="color:inherit;font-size:14.5px;font-weight:730;letter-spacing:var(--tracking-tight)">Workspace</b> <span slot="footer" style="color:inherit;font-size:12px">Dana Whitfield</span> <span slot="footer-collapsed" aria-label="Dana Whitfield" style="width:26px;height:26px;border-radius:50%;background:var(--accent);color:var(--accent-contrast);font-size:9px;font-weight:720;display:inline-flex;align-items:center;justify-content:center;user-select:none">DW</span> </sui-side-nav> <script type="module"> import { defineCustomElements } from "@shineui/core/loader"; import "@shineui/core/shineui.css"; defineCustomElements(); const el = document.querySelector("sui-side-nav"); el.sections = [ { items: [ { label: "Home", icon: "fa-solid fa-house" }, { label: "Projects", icon: "fa-regular fa-folder-open", badge: 4 }, { label: "Tasks", icon: "fa-regular fa-square-check" }, { label: "Calendar", icon: "fa-regular fa-calendar" }, ], }, { heading: "Workspace", items: [{ label: "Members", icon: "fa-regular fa-user" }, { label: "Reports", icon: "fa-solid fa-chart-column" }] }, ]; /* SuiSideNavSection[] */ el.addEventListener("suiClose", (e) => console.log(e.detail)); </script>
import { SuiSideNav } from "@shineui/react"; import "@shineui/core/shineui.css"; export function Example() { const sections = [ { items: [ { label: "Home", icon: "fa-solid fa-house" }, { label: "Projects", icon: "fa-regular fa-folder-open", badge: 4 }, { label: "Tasks", icon: "fa-regular fa-square-check" }, { label: "Calendar", icon: "fa-regular fa-calendar" }, ], }, { heading: "Workspace", items: [{ label: "Members", icon: "fa-regular fa-user" }, { label: "Reports", icon: "fa-solid fa-chart-column" }] }, ]; /* SuiSideNavSection[] */ return ( <SuiSideNav aria-label="Primary" height="360" active="Projects" sections={sections} onSuiClose={(e) => console.log(e.detail)}> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" fill="none" slot="icon" aria-hidden="true" focusable="false" style="width:28px;height:28px;display:block"> <rect width="32" height="32" rx="9" fill="var(--accent)"/> <g fill="var(--accent-contrast)"> <path d="M12.5 6.5H23A2.5 2.5 0 0 1 25.5 9V10.5A2.5 2.5 0 0 1 23 13H15.5A2.5 2.5 0 0 0 13 15.5V23A2.5 2.5 0 0 1 10.5 25.5H9A2.5 2.5 0 0 1 6.5 23V12.5A6 6 0 0 1 12.5 6.5Z"/> <rect x="17.5" y="17.5" width="8" height="8" rx="3"/> </g> </svg> <b slot="brand" style="color:inherit;font-size:14.5px;font-weight:730;letter-spacing:var(--tracking-tight)">Workspace</b> <span slot="footer" style="color:inherit;font-size:12px">Dana Whitfield</span> <span slot="footer-collapsed" aria-label="Dana Whitfield" style="width:26px;height:26px;border-radius:50%;background:var(--accent);color:var(--accent-contrast);font-size:9px;font-weight:720;display:inline-flex;align-items:center;justify-content:center;user-select:none">DW</span> </SuiSideNav> ); }
<script setup> import { SuiSideNav } from "@shineui/vue"; import "@shineui/core/shineui.css"; const sections = [ { items: [ { label: "Home", icon: "fa-solid fa-house" }, { label: "Projects", icon: "fa-regular fa-folder-open", badge: 4 }, { label: "Tasks", icon: "fa-regular fa-square-check" }, { label: "Calendar", icon: "fa-regular fa-calendar" }, ], }, { heading: "Workspace", items: [{ label: "Members", icon: "fa-regular fa-user" }, { label: "Reports", icon: "fa-solid fa-chart-column" }] }, ]; // SuiSideNavSection[] const onChange = (e) => console.log(e.detail); </script> <template> <SuiSideNav aria-label="Primary" height="360" active="Projects" :sections="sections" @sui-close="onChange"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" fill="none" slot="icon" aria-hidden="true" focusable="false" style="width:28px;height:28px;display:block"> <rect width="32" height="32" rx="9" fill="var(--accent)"/> <g fill="var(--accent-contrast)"> <path d="M12.5 6.5H23A2.5 2.5 0 0 1 25.5 9V10.5A2.5 2.5 0 0 1 23 13H15.5A2.5 2.5 0 0 0 13 15.5V23A2.5 2.5 0 0 1 10.5 25.5H9A2.5 2.5 0 0 1 6.5 23V12.5A6 6 0 0 1 12.5 6.5Z"/> <rect x="17.5" y="17.5" width="8" height="8" rx="3"/> </g> </svg> <b slot="brand" style="color:inherit;font-size:14.5px;font-weight:730;letter-spacing:var(--tracking-tight)">Workspace</b> <span slot="footer" style="color:inherit;font-size:12px">Dana Whitfield</span> <span slot="footer-collapsed" aria-label="Dana Whitfield" style="width:26px;height:26px;border-radius:50%;background:var(--accent);color:var(--accent-contrast);font-size:9px;font-weight:720;display:inline-flex;align-items:center;justify-content:center;user-select:none">DW</span></SuiSideNav> </template>
import { Component } from "@angular/core"; import { SuiSideNav } from "@shineui/angular"; @Component({ standalone: true, imports: [SuiSideNav], template: `<sui-side-nav aria-label="Primary" height="360" active="Projects" [(collapsed)]="collapsed" [sections]="sections" (suiClose)="onChange($event)"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" fill="none" slot="icon" aria-hidden="true" focusable="false" style="width:28px;height:28px;display:block"> <rect width="32" height="32" rx="9" fill="var(--accent)"/> <g fill="var(--accent-contrast)"> <path d="M12.5 6.5H23A2.5 2.5 0 0 1 25.5 9V10.5A2.5 2.5 0 0 1 23 13H15.5A2.5 2.5 0 0 0 13 15.5V23A2.5 2.5 0 0 1 10.5 25.5H9A2.5 2.5 0 0 1 6.5 23V12.5A6 6 0 0 1 12.5 6.5Z"/> <rect x="17.5" y="17.5" width="8" height="8" rx="3"/> </g> </svg> <b slot="brand" style="color:inherit;font-size:14.5px;font-weight:730;letter-spacing:var(--tracking-tight)">Workspace</b> <span slot="footer" style="color:inherit;font-size:12px">Dana Whitfield</span> <span slot="footer-collapsed" aria-label="Dana Whitfield" style="width:26px;height:26px;border-radius:50%;background:var(--accent);color:var(--accent-contrast);font-size:9px;font-weight:720;display:inline-flex;align-items:center;justify-content:center;user-select:none">DW</span></sui-side-nav>`, }) export class Example { collapsed = false; sections = [ { items: [ { label: "Home", icon: "fa-solid fa-house" }, { label: "Projects", icon: "fa-regular fa-folder-open", badge: 4 }, { label: "Tasks", icon: "fa-regular fa-square-check" }, { label: "Calendar", icon: "fa-regular fa-calendar" }, ], }, { heading: "Workspace", items: [{ label: "Members", icon: "fa-regular fa-user" }, { label: "Reports", icon: "fa-solid fa-chart-column" }] }, ]; // SuiSideNavSection[] onChange(e: CustomEvent) { console.log(e.detail); } }

Basic

The rail is a nav landmark named Primary, and its items sit in one list per section so a reader hears how many there are. The active item carries aria-current="page" and a stronger wash than the hover. This is navigation, not an application menu, so there is no roving tabindex and no arrow-key map.

Workspace Dana Whitfield DW
Projects
Everything the team has open.
Onboarding refresh 2 Sep
Warehouse migration 28 Aug
Quarterly planning 19 Aug
<div style="display:flex;gap:16px;align-items:stretch"> <sui-side-nav aria-label="Primary" height="360" active="Projects"> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" fill="none" slot="icon" aria-hidden="true" focusable="false" style="width:28px;height:28px;display:block"> <rect width="32" height="32" rx="9" fill="var(--accent)"/> <g fill="var(--accent-contrast)"> <path d="M12.5 6.5H23A2.5 2.5 0 0 1 25.5 9V10.5A2.5 2.5 0 0 1 23 13H15.5A2.5 2.5 0 0 0 13 15.5V23A2.5 2.5 0 0 1 10.5 25.5H9A2.5 2.5 0 0 1 6.5 23V12.5A6 6 0 0 1 12.5 6.5Z"/> <rect x="17.5" y="17.5" width="8" height="8" rx="3"/> </g> </svg> <b slot="brand" style="color:inherit;font-size:14.5px;font-weight:730;letter-spacing:var(--tracking-tight)">Workspace</b> <span slot="footer" style="color:inherit;font-size:12px">Dana Whitfield</span> <span slot="footer-collapsed" aria-label="Dana Whitfield" style="width:26px;height:26px;border-radius:50%;background:var(--accent);color:var(--accent-contrast);font-size:9px;font-weight:720;display:inline-flex;align-items:center;justify-content:center;user-select:none">DW</span> </sui-side-nav> <div style="flex:1;min-width:0;background:var(--surface);border:1px solid var(--border);border-radius:var(--radius-lg);padding:18px 20px;box-sizing:border-box"> <div style="font-size:17px;font-weight:700;color:var(--ink)">Projects</div> <div style="font-size:13px;color:var(--ink-muted);margin:3px 0 14px">Everything the team has open.</div> <div style="display:flex;align-items:center;justify-content:space-between;gap:12px;padding:11px 0;border-top:1px solid var(--hairline)"> <span style="font-size:13.5px;font-weight:550;color:var(--ink)">Onboarding refresh</span> <span style="font-size:12px;color:var(--ink-muted);font-feature-settings:'tnum'">2 Sep</span> </div> <div style="display:flex;align-items:center;justify-content:space-between;gap:12px;padding:11px 0;border-top:1px solid var(--hairline)"> <span style="font-size:13.5px;font-weight:550;color:var(--ink)">Warehouse migration</span> <span style="font-size:12px;color:var(--ink-muted);font-feature-settings:'tnum'">28 Aug</span> </div> <div style="display:flex;align-items:center;justify-content:space-between;gap:12px;padding:11px 0;border-top:1px solid var(--hairline)"> <span style="font-size:13.5px;font-weight:550;color:var(--ink)">Quarterly planning</span> <span style="font-size:12px;color:var(--ink-muted);font-feature-settings:'tnum'">19 Aug</span> </div> </div> </div>
// setup(root): properties and events setup(root) { root.querySelector('sui-side-nav').sections = nav(); }

Grounds

Three grounds, all neutral. brand is the chrome from the theme primary, the same in both modes: deep under light ink, or a light primary under dark ink. surface matches the content cards. canvas recesses the rail onto the app background so the content floats over it. The current item wears a stronger wash than a hover.

Workspace Workspace Workspace
<div style="display:flex;gap:14px;align-items:flex-start"> <sui-side-nav aria-label="Primary, brand" id="sn-brand" tone="brand" height="340" active="Projects"> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" fill="none" slot="icon" aria-hidden="true" focusable="false" style="width:28px;height:28px;display:block"> <rect width="32" height="32" rx="9" fill="var(--accent)"/> <g fill="var(--accent-contrast)"> <path d="M12.5 6.5H23A2.5 2.5 0 0 1 25.5 9V10.5A2.5 2.5 0 0 1 23 13H15.5A2.5 2.5 0 0 0 13 15.5V23A2.5 2.5 0 0 1 10.5 25.5H9A2.5 2.5 0 0 1 6.5 23V12.5A6 6 0 0 1 12.5 6.5Z"/> <rect x="17.5" y="17.5" width="8" height="8" rx="3"/> </g> </svg> <b slot="brand" style="color:inherit;font-size:14.5px;font-weight:730;letter-spacing:var(--tracking-tight)">Workspace</b> </sui-side-nav> <sui-side-nav aria-label="Primary, surface" id="sn-surface" tone="surface" height="340" active="Projects"> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" fill="none" slot="icon" aria-hidden="true" focusable="false" style="width:28px;height:28px;display:block"> <rect width="32" height="32" rx="9" fill="var(--accent)"/> <g fill="var(--accent-contrast)"> <path d="M12.5 6.5H23A2.5 2.5 0 0 1 25.5 9V10.5A2.5 2.5 0 0 1 23 13H15.5A2.5 2.5 0 0 0 13 15.5V23A2.5 2.5 0 0 1 10.5 25.5H9A2.5 2.5 0 0 1 6.5 23V12.5A6 6 0 0 1 12.5 6.5Z"/> <rect x="17.5" y="17.5" width="8" height="8" rx="3"/> </g> </svg> <b slot="brand" style="color:inherit;font-size:14.5px;font-weight:730;letter-spacing:var(--tracking-tight)">Workspace</b> </sui-side-nav> <sui-side-nav aria-label="Primary, canvas" id="sn-canvas" tone="canvas" height="340" active="Projects"> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" fill="none" slot="icon" aria-hidden="true" focusable="false" style="width:28px;height:28px;display:block"> <rect width="32" height="32" rx="9" fill="var(--accent)"/> <g fill="var(--accent-contrast)"> <path d="M12.5 6.5H23A2.5 2.5 0 0 1 25.5 9V10.5A2.5 2.5 0 0 1 23 13H15.5A2.5 2.5 0 0 0 13 15.5V23A2.5 2.5 0 0 1 10.5 25.5H9A2.5 2.5 0 0 1 6.5 23V12.5A6 6 0 0 1 12.5 6.5Z"/> <rect x="17.5" y="17.5" width="8" height="8" rx="3"/> </g> </svg> <b slot="brand" style="color:inherit;font-size:14.5px;font-weight:730;letter-spacing:var(--tracking-tight)">Workspace</b> </sui-side-nav> </div>
// setup(root): properties and events setup(root) { root.querySelector('#sn-brand').sections = nav(); root.querySelector('#sn-surface').sections = nav(); root.querySelector('#sn-canvas').sections = nav(); }

Card and flush

variant="flush" squares the corners and rules only the trailing edge, for a rail that butts against a toolbar and the content. The default card keeps its rounding, which is right when the rail stands alone with space around it. Squaring is what stops the rounded corners cutting wedges out of the shell.

Workspace
Workspace / Projects
Projects
Onboarding refresh 2 Sep
Warehouse migration 28 Aug
Workspace
<div style="display:flex;gap:14px;align-items:flex-start;flex-wrap:wrap"> <div style="flex:1 1 340px;min-width:0;display:flex;align-items:stretch;border:1px solid var(--border);border-radius:var(--radius-lg);overflow:hidden;background:var(--canvas)"> <sui-side-nav aria-label="Primary, flush" id="sn-flush" variant="flush" height="330" active="Projects"> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" fill="none" slot="icon" aria-hidden="true" focusable="false" style="width:28px;height:28px;display:block"> <rect width="32" height="32" rx="9" fill="var(--accent)"/> <g fill="var(--accent-contrast)"> <path d="M12.5 6.5H23A2.5 2.5 0 0 1 25.5 9V10.5A2.5 2.5 0 0 1 23 13H15.5A2.5 2.5 0 0 0 13 15.5V23A2.5 2.5 0 0 1 10.5 25.5H9A2.5 2.5 0 0 1 6.5 23V12.5A6 6 0 0 1 12.5 6.5Z"/> <rect x="17.5" y="17.5" width="8" height="8" rx="3"/> </g> </svg> <b slot="brand" style="color:inherit;font-size:14.5px;font-weight:730;letter-spacing:var(--tracking-tight)">Workspace</b> </sui-side-nav> <div style="flex:1;min-width:0;display:flex;flex-direction:column;background:var(--surface)"> <sui-toolbar variant="flush" aria-label="Project actions"> <span slot="start" style="font-size:13px;font-weight:620;color:var(--ink)">Workspace / Projects</span> <sui-button slot="end" variant="accent" size="sm" icon="fa-solid fa-plus" label="New"></sui-button> </sui-toolbar> <div style="padding:14px;display:flex;flex-direction:column;gap:10px"> <div style="font-size:15px;font-weight:700;color:var(--ink)">Projects</div> <div style="display:flex;align-items:center;justify-content:space-between;gap:12px;padding:11px 0;border-top:1px solid var(--hairline)"> <span style="font-size:13.5px;font-weight:550;color:var(--ink)">Onboarding refresh</span> <span style="font-size:12px;color:var(--ink-muted);font-feature-settings:'tnum'">2 Sep</span> </div> <div style="display:flex;align-items:center;justify-content:space-between;gap:12px;padding:11px 0;border-top:1px solid var(--hairline)"> <span style="font-size:13.5px;font-weight:550;color:var(--ink)">Warehouse migration</span> <span style="font-size:12px;color:var(--ink-muted);font-feature-settings:'tnum'">28 Aug</span> </div> </div> </div> </div> <sui-side-nav aria-label="Primary, card" id="sn-card" height="330" active="Projects"> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" fill="none" slot="icon" aria-hidden="true" focusable="false" style="width:28px;height:28px;display:block"> <rect width="32" height="32" rx="9" fill="var(--accent)"/> <g fill="var(--accent-contrast)"> <path d="M12.5 6.5H23A2.5 2.5 0 0 1 25.5 9V10.5A2.5 2.5 0 0 1 23 13H15.5A2.5 2.5 0 0 0 13 15.5V23A2.5 2.5 0 0 1 10.5 25.5H9A2.5 2.5 0 0 1 6.5 23V12.5A6 6 0 0 1 12.5 6.5Z"/> <rect x="17.5" y="17.5" width="8" height="8" rx="3"/> </g> </svg> <b slot="brand" style="color:inherit;font-size:14.5px;font-weight:730;letter-spacing:var(--tracking-tight)">Workspace</b> </sui-side-nav> </div>
// setup(root): properties and events setup(root) { root.querySelector('#sn-flush').sections = nav(); root.querySelector('#sn-card').sections = nav(); }

Icon rail

The icon slot shows in both states, so the mark is there when the labels go and a 60px rail still reads as an application. The wordmark in brand goes. A collapsed item keeps its name through aria-label and shows a capsule on hover and on focus, which Escape dismisses. collapsed-badges="dot" keeps the count.

Workspace Dana Whitfield DW Workspace Dana Whitfield DW
<div style="display:flex;gap:14px;align-items:flex-start"> <sui-side-nav aria-label="Primary, expanded" id="sn-open" height="340" active="Projects"> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" fill="none" slot="icon" aria-hidden="true" focusable="false" style="width:28px;height:28px;display:block"> <rect width="32" height="32" rx="9" fill="var(--accent)"/> <g fill="var(--accent-contrast)"> <path d="M12.5 6.5H23A2.5 2.5 0 0 1 25.5 9V10.5A2.5 2.5 0 0 1 23 13H15.5A2.5 2.5 0 0 0 13 15.5V23A2.5 2.5 0 0 1 10.5 25.5H9A2.5 2.5 0 0 1 6.5 23V12.5A6 6 0 0 1 12.5 6.5Z"/> <rect x="17.5" y="17.5" width="8" height="8" rx="3"/> </g> </svg> <b slot="brand" style="color:inherit;font-size:14.5px;font-weight:730;letter-spacing:var(--tracking-tight)">Workspace</b> <span slot="footer" style="color:inherit;font-size:12px">Dana Whitfield</span> <span slot="footer-collapsed" aria-label="Dana Whitfield" style="width:26px;height:26px;border-radius:50%;background:var(--accent);color:var(--accent-contrast);font-size:9px;font-weight:720;display:inline-flex;align-items:center;justify-content:center;user-select:none">DW</span> </sui-side-nav> <sui-side-nav aria-label="Primary, collapsed" id="sn-shut" height="340" collapsed collapsed-badges="dot" active="Projects"> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" fill="none" slot="icon" aria-hidden="true" focusable="false" style="width:28px;height:28px;display:block"> <rect width="32" height="32" rx="9" fill="var(--accent)"/> <g fill="var(--accent-contrast)"> <path d="M12.5 6.5H23A2.5 2.5 0 0 1 25.5 9V10.5A2.5 2.5 0 0 1 23 13H15.5A2.5 2.5 0 0 0 13 15.5V23A2.5 2.5 0 0 1 10.5 25.5H9A2.5 2.5 0 0 1 6.5 23V12.5A6 6 0 0 1 12.5 6.5Z"/> <rect x="17.5" y="17.5" width="8" height="8" rx="3"/> </g> </svg> <b slot="brand" style="color:inherit;font-size:14.5px;font-weight:730;letter-spacing:var(--tracking-tight)">Workspace</b> <span slot="footer" style="color:inherit;font-size:12px">Dana Whitfield</span> <span slot="footer-collapsed" aria-label="Dana Whitfield" style="width:26px;height:26px;border-radius:50%;background:var(--accent);color:var(--accent-contrast);font-size:9px;font-weight:720;display:inline-flex;align-items:center;justify-content:center;user-select:none">DW</span> </sui-side-nav> </div>
// setup(root): properties and events setup(root) { root.querySelector('#sn-open').sections = nav(); root.querySelector('#sn-shut').sections = nav(); }

Groups and badges

An item with children becomes the group's disclosure button: it takes aria-expanded, opens on Enter, and emits suiExpandedChange with the open keys rather than suiNavigate. On the collapsed rail the same button opens a flyout of the children beside the icon. A disabled item is an inert button whatever href says.

Workspace Open groups: Settings.
<div style="display:flex;gap:16px;align-items:flex-start"> <sui-side-nav aria-label="Primary, Groups" id="sn-groups" height="350" active="Permissions"> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" fill="none" slot="icon" aria-hidden="true" focusable="false" style="width:28px;height:28px;display:block"> <rect width="32" height="32" rx="9" fill="var(--accent)"/> <g fill="var(--accent-contrast)"> <path d="M12.5 6.5H23A2.5 2.5 0 0 1 25.5 9V10.5A2.5 2.5 0 0 1 23 13H15.5A2.5 2.5 0 0 0 13 15.5V23A2.5 2.5 0 0 1 10.5 25.5H9A2.5 2.5 0 0 1 6.5 23V12.5A6 6 0 0 1 12.5 6.5Z"/> <rect x="17.5" y="17.5" width="8" height="8" rx="3"/> </g> </svg> <b slot="brand" style="color:inherit;font-size:14.5px;font-weight:730;letter-spacing:var(--tracking-tight)">Workspace</b> </sui-side-nav> <span id="sn-groups-log" style="font:460 13px/1.6 var(--font-sans);color:var(--ink-muted)">Open groups: Settings.</span> </div>
// setup(root): properties and events setup(root) { const el = root.querySelector('#sn-groups'); const log = root.querySelector('#sn-groups-log'); el.sections = grouped(); el.addEventListener('suiExpandedChange', (e) => { log.textContent = e.detail.length ? `Open groups: ${e.detail.join(', ')}.` : 'No open groups.'; }); }

The header slot holds the one accent action, a search box or a workspace switcher, and swaps for header-collapsed on the icon rail. A section marked bottom drops to the foot of the list, which is where Settings and Help belong. The footer slot shares its strip with the collapse control.

Workspace Dana Whitfield DW Workspace Dana Whitfield DW
<div style="display:flex;gap:14px;align-items:flex-start"> <sui-side-nav aria-label="Primary, with header" id="sn-head" height="340" active="Projects"> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" fill="none" slot="icon" aria-hidden="true" focusable="false" style="width:28px;height:28px;display:block"> <rect width="32" height="32" rx="9" fill="var(--accent)"/> <g fill="var(--accent-contrast)"> <path d="M12.5 6.5H23A2.5 2.5 0 0 1 25.5 9V10.5A2.5 2.5 0 0 1 23 13H15.5A2.5 2.5 0 0 0 13 15.5V23A2.5 2.5 0 0 1 10.5 25.5H9A2.5 2.5 0 0 1 6.5 23V12.5A6 6 0 0 1 12.5 6.5Z"/> <rect x="17.5" y="17.5" width="8" height="8" rx="3"/> </g> </svg> <b slot="brand" style="color:inherit;font-size:14.5px;font-weight:730;letter-spacing:var(--tracking-tight)">Workspace</b> <sui-button slot="header" variant="accent" size="sm" block icon="fa-solid fa-plus" label="New project"></sui-button> <sui-icon-button slot="header-collapsed" variant="ghost" icon="fa-solid fa-plus" aria-label="New project"></sui-icon-button> <span slot="footer" style="color:inherit;font-size:12px">Dana Whitfield</span> <span slot="footer-collapsed" aria-label="Dana Whitfield" style="width:26px;height:26px;border-radius:50%;background:var(--accent);color:var(--accent-contrast);font-size:9px;font-weight:720;display:inline-flex;align-items:center;justify-content:center;user-select:none">DW</span> </sui-side-nav> <sui-side-nav aria-label="Primary, with header collapsed" id="sn-head-shut" height="340" collapsed active="Projects"> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" fill="none" slot="icon" aria-hidden="true" focusable="false" style="width:28px;height:28px;display:block"> <rect width="32" height="32" rx="9" fill="var(--accent)"/> <g fill="var(--accent-contrast)"> <path d="M12.5 6.5H23A2.5 2.5 0 0 1 25.5 9V10.5A2.5 2.5 0 0 1 23 13H15.5A2.5 2.5 0 0 0 13 15.5V23A2.5 2.5 0 0 1 10.5 25.5H9A2.5 2.5 0 0 1 6.5 23V12.5A6 6 0 0 1 12.5 6.5Z"/> <rect x="17.5" y="17.5" width="8" height="8" rx="3"/> </g> </svg> <b slot="brand" style="color:inherit;font-size:14.5px;font-weight:730;letter-spacing:var(--tracking-tight)">Workspace</b> <sui-button slot="header" variant="accent" size="sm" block icon="fa-solid fa-plus" label="New project"></sui-button> <sui-icon-button slot="header-collapsed" variant="ghost" icon="fa-solid fa-plus" aria-label="New project"></sui-icon-button> <span slot="footer" style="color:inherit;font-size:12px">Dana Whitfield</span> <span slot="footer-collapsed" aria-label="Dana Whitfield" style="width:26px;height:26px;border-radius:50%;background:var(--accent);color:var(--accent-contrast);font-size:9px;font-weight:720;display:inline-flex;align-items:center;justify-content:center;user-select:none">DW</span> </sui-side-nav> </div>
// setup(root): properties and events setup(root) { root.querySelector('#sn-head').sections = withFoot(); root.querySelector('#sn-head-shut').sections = withFoot(); }

Rail width

Long labels wrap onto a second line at the default width. Set width to give them a line of their own, and collapsed-width for the icon rail. Leave both unset and the rail keeps the --sidebar-w token, so a page that retunes that token keeps its own measure.

Workspace Workspace
<div style="display:flex;gap:14px;align-items:flex-start"> <sui-side-nav aria-label="Primary, default width" id="sn-narrow" height="250" active="Warehouse transfer requests"> <b slot="brand" style="color:inherit;font-size:14.5px;font-weight:730;letter-spacing:var(--tracking-tight)">Workspace</b> </sui-side-nav> <sui-side-nav aria-label="Primary, wide" id="sn-wide" width="300" collapsed-width="76" height="250" active="Warehouse transfer requests"> <b slot="brand" style="color:inherit;font-size:14.5px;font-weight:730;letter-spacing:var(--tracking-tight)">Workspace</b> </sui-side-nav> </div>
// setup(root): properties and events setup(root) { root.querySelector('#sn-narrow').sections = roomy(); root.querySelector('#sn-wide').sections = roomy(); }

Events and an outside control

The rail writes collapsed itself, so it emits suiOpen after it expands and the cancelable suiClose before it collapses. Set collapse-control="none" when the shell owns the toggle, then drive it with the collapsed property or toggle(). The button below also answers the bracket key, which is how an app binds a shortcut.

Workspace Dana Whitfield DW
Expanded, on Projects. Press the bracket key with the demo focused.
<div style="display:flex;gap:16px;align-items:flex-start"> <sui-side-nav aria-label="Primary, Events" id="sn-events" height="340" collapse-control="none" active="Projects"> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" fill="none" slot="icon" aria-hidden="true" focusable="false" style="width:28px;height:28px;display:block"> <rect width="32" height="32" rx="9" fill="var(--accent)"/> <g fill="var(--accent-contrast)"> <path d="M12.5 6.5H23A2.5 2.5 0 0 1 25.5 9V10.5A2.5 2.5 0 0 1 23 13H15.5A2.5 2.5 0 0 0 13 15.5V23A2.5 2.5 0 0 1 10.5 25.5H9A2.5 2.5 0 0 1 6.5 23V12.5A6 6 0 0 1 12.5 6.5Z"/> <rect x="17.5" y="17.5" width="8" height="8" rx="3"/> </g> </svg> <b slot="brand" style="color:inherit;font-size:14.5px;font-weight:730;letter-spacing:var(--tracking-tight)">Workspace</b> <span slot="footer" style="color:inherit;font-size:12px">Dana Whitfield</span> <span slot="footer-collapsed" aria-label="Dana Whitfield" style="width:26px;height:26px;border-radius:50%;background:var(--accent);color:var(--accent-contrast);font-size:9px;font-weight:720;display:inline-flex;align-items:center;justify-content:center;user-select:none">DW</span> </sui-side-nav> <div style="display:flex;flex-direction:column;gap:10px;align-items:flex-start"> <sui-button id="sn-toggle" variant="outline" size="sm" icon="fa-solid fa-bars" label="Toggle navigation"></sui-button> <span id="sn-log" style="font:460 13px/1.6 var(--font-sans);color:var(--ink-muted)">Expanded, on Projects. Press the bracket key with the demo focused.</span> </div> </div>
// setup(root): properties and events setup(root) { const log = root.querySelector('#sn-log'); const el = root.querySelector('#sn-events'); const btn = root.querySelector('#sn-toggle'); el.sections = nav(); el.addEventListener('suiNavigate', (e) => { log.textContent = `Navigated to ${e.detail}.`; }); el.addEventListener('suiOpen', () => { log.textContent = 'Expanded.'; }); el.addEventListener('suiClose', (e) => { log.textContent = `Collapsing, reason ${e.detail}.`; }); btn.addEventListener('click', () => el.toggle()); root.addEventListener('keydown', (e) => { if (e.key === '[' && !e.metaKey && !e.ctrlKey && !e.altKey) el.toggle(); }); }

Keyboard

KeyAction
Tab Shift TabMoves through the header, every item, the footer and the collapse control in document order. Nothing is skipped, because the rail keeps no roving tabindex.
EnterFollows the focused link, activates the focused item (suiActiveChange when it was not active, then suiNavigate), opens and closes the focused group, or operates the collapse control.
SpaceActivates the focused item, group or control. On an item with href the browser scrolls the page, as it does for any link.

API

<sui-side-nav>

Properties

PropertyAttributeTypeDefaultDescription
activeactivestringstring | undefinedKey of the active item, its own key or its label. Mutable; updated on item activation and reported through suiActiveChange.
collapseControlcollapse-control"footer" | "none"'footer'Where the rail's own collapse control sits. footer is the default: one button in the foot of the rail, always visible, never hidden behind a hover, in the same place whether the rail is expanded or collapsed. none removes it, for a shell that toggles the rail from its own header instead; write collapsed or call toggle() from there.
collapsedcollapsedbooleanfalseIcon-only mode, 60px wide unless collapsedWidth says otherwise. Mutable; the rail's own control toggles it. A collapsed item keeps its name through aria-label and shows a label capsule on hover and focus; its badge goes, or becomes a dot, as collapsedBadges says.
collapsedBadgescollapsed-badges"dot" | "hide"'hide'What a badge becomes on a collapsed item, where there is no room for the count. hide drops it, which is the default. dot draws a marker on the icon and reads the count into the item's aria-label, so the signal survives the collapse.
collapsedWidthcollapsed-widthnumbernumber | undefinedCollapsed width in px. Unset keeps the 60px icon rail.
expandedproperty onlystring[][]Keys of the open groups, spelled as on every element that holds an outline (sui-tree, sui-tree-table, sui-panel-menu, sui-org-chart). Mutable; the element updates it as parents toggle, and opens the group that holds active when a write from script moves active into a closed one. A row chosen in the collapsed rail's flyout leaves its group closed, the parent marked as holding the current page. Property, not attribute.
heightheightnumbernumber | undefinedRail height in px. Leave it unset in an application shell and the rail fills the row it sits in, which is what variant="flush" is for; a demo on a page sets a number instead.
sectionsproperty onlySuiSideNavSection[][]Sections to render: { heading?, divider?, bottom?, items: [{ label, icon?, badge?, href?, target?, disabled?, children?, command? }] }[]. Items are real links when they carry href and buttons with role="link" otherwise, in document order inside one list per section, with no roving tabindex and no arrow-key map: this is site navigation, not an application menu. Property, not attribute.
tonetone"brand" | "canvas" | "surface"'canvas'Which ground the rail paints, in every theme and in both light and dark mode. canvas is the default: the app background, one step recessed from the content, so the cards float above the rail the way Linear, Notion and VS Code lay out. surface is the same ground as the content cards, under a hairline. brand is the chrome built from the theme's primary: a deep tone under light ink when the primary is dark, the primary itself under dark ink when it is light, and the same in both modes; a quiet control inside it needs its own inverse tone. Anything else falls back to canvas.
variantvariant"card" | "flush"'card'Shape of the rail. card is the default standalone box: rounded corners, and a border on the light tones, for a rail that sits on a page with space around it. flush squares the corners and keeps a single rule down the trailing edge, for a rail that butts against a toolbar and the content in an application shell. Anything else falls back to card.
widthwidthnumbernumber | undefinedExpanded width in px. Unset keeps the --sidebar-w token, so a page that already retunes that token keeps its width. Set it when one rail needs a width of its own, for long labels.

Events

EventDetailReactVueDescription
suiActiveChangestringonSuiActiveChange@sui-active-changeFires whenever the element writes active, a choice of an item that was not active, before suiNavigate; detail is the new active. Writing active from script emits nothing. Does not bubble.
suiCloseSuiCloseReason"cancel" | "close-button" | "escape" | "outside" | "pick" | "scrim" | "trigger"onSuiClose@sui-closeFires before the rail collapses itself; detail is 'trigger' (its own control and toggle() are the only ways it collapses). Cancelable: preventDefault() keeps the rail expanded. It does not bubble.
suiExpandedChangestring[]onSuiExpandedChange@sui-expanded-changeFires after a group parent opens or closes, or when a script write moves active into a closed group and the element opens it; detail is the new expanded array of open keys. Group expansion is indexed state, not open state, so it reports the way sui-accordion and sui-panel-menu do. Does not bubble.
suiNavigatestringonSuiNavigate@sui-navigateFires when an item is activated; detail is the item key, its own key or its label.
suiOpenvoidonSuiOpen@sui-openFires after the rail expands itself. It does not bubble, as no open-state event does.

Methods

MethodSignatureDescription
setFocus()setFocus() => Promise<void>Moves keyboard focus to the active item, or to the group that holds it while that group is closed, else the first enabled item.
toggle()toggle(next?: boolean) => Promise<void>Collapses or expands the rail from outside, reporting exactly as the rail's own control does: suiOpen after it expands, the cancelable suiClose before it collapses. Pass nothing to flip the current state. This is the hook for an application that binds its own keyboard shortcut or puts the toggle in its header, which is why the element binds no global key of its own.

Slots

SlotDescription
brandThe wordmark beside the mark, expanded only. It goes when the rail collapses; the icon slot is what carries the branding there.
footerFooter content when expanded (typically a persona row).
footer-collapsedShown instead of footer when collapsed (typically an avatar); omit to hide the footer content. The collapse control keeps its place either way.
headerRow under the brand, above the items: a search box, a workspace switcher, or the one accent action.
header-collapsedShown instead of header when collapsed (typically one icon button); omit to hide the header row.
iconThe workspace or product mark, shown in BOTH states: beside the wordmark when expanded, alone and centered on the icon rail. Draw it around 28px square.

Types

SuiCloseReason typeWhy a dismissible surface is closing. Detail of the cancelable suiClose. A consumer expresses "a stray backdrop click must not close this" by calling preventDefault() on the reason it does not accept, which is why none of these needs a prop of its own.
No members.
SuiSideNavItem interfaceOne rail item: label, optional icon class and badge count.
labelstring
key?stringIdentity for active and suiNavigate. Defaults to the label; set it once the labels are translated or edited, so a handler keys off something that does not move.
icon?string
badge?string | number
href?stringRenders the item as a real link, so middle-click and open in a new tab work and Enter follows it. suiNavigate still fires and active still updates on the click.
target?stringLink target for an item with href, e.g. _blank. A _blank item gets rel="noopener".
disabled?booleanInert item, for a destination the account cannot reach. Renders a disabled button whatever href says, emits nothing and takes no aria-current.
children?SuiSideNavItem[]Child items, one level deep. The parent stops being a destination and becomes the group's disclosure button: it carries aria-expanded, opens and closes on Enter or Space, and emits suiExpandedChange rather than suiNavigate. On the collapsed rail it opens its children in a flyout beside the icon instead, and the rail stays collapsed. href on such an item is ignored.
command?() => voidRuns when the item is chosen, alongside suiNavigate. A group parent never runs it.
SuiSideNavSection interfaceOne rail section: optional eyebrow heading plus its items.
heading?string
itemsSuiSideNavItem[]
divider?booleanDraws a hairline above the section. A collapsed rail already rules between headed sections, so this is the way to separate two sections that carry no heading.
bottom?booleanPushes this section and everything after it to the foot of the rail, above the footer slot. This is where Settings and Help live. Only the first section that sets it takes the space.

Theming

Every design token this element can render with, read from its own stylesheets and the ones it imports. Set a token on :root or on any ancestor and every use below follows it. The values shown are the ones in the theme you are viewing. The list is read per stylesheet, so an element that imports a shared drawing module lists that module's tokens too, whether or not it paints with all of them.

Color24

--accentthe one chart highlight: the flagged series, down candles, the peak cell--accent-contrasttext and glyphs sitting on solid accent fills--accent-inverse--accent-markthe accent drawn as a mark on a page: active bars, sort arrows, the logotype--bordercard + table outer borders--border-inverse--canvasapp background--focus-ring--hairlinerow separators--ink--ink-inverse--ink-inverse-muted--ink-mutedsecondary text: 7:1 on the page grounds and 1.5 times faint--ink-muted-on-washthe muted ink a selected, current or pressed row paints: 4.5:1 on that wash--ink-placeholdera field's placeholder, not a text tier: 4.5:1 on the page grounds--logo-quiet-inversethe quiet pills' share of the ink on the chrome--logo-spark-inversethe same spark, fitted to the navy chrome--surfacecards, tables, inputs--surface-currenta navigation's current item: the ink, translucent--surface-hovera hovered row or quiet control on any ground: the ink, translucent--surface-navysidebar, GL account bars, inverse chrome--surface-navy-currentthe same on the brand ground, solved per theme--surface-raisedmenus, popovers, toasts: the card in light, a step above it in dark--surface-sunkentable headers, wells

Typography2

Ag--font-sans0.12--tracking-widernav section labels, eyebrows

Spacing and size4

28--control-sm228--sidebar-w700--z-dropdown1100--z-tooltip

Radius2

--radius-lgcards, tables, panels, dialogs--radius-mdALL controls: inputs, buttons, selects

Elevation2

--focus-shadow--shadow-popdropdowns, popovers, toasts

Motion3

--duration-basetoggles, chevrons, tabs, disclosure heights--duration-fasthovers, presses, every exit--ease-out

Every name above is listed with its value and its role in Foundations.

See also