Sidebar
The side navigation of an app, open while people work.
In Dense data
See it live
npm run storybookWhen to use it
In Dense data: App navigation that stays open all shift.
- Use it when: Apps with several sections people move between all session.
- Not when: A website's main navigation (navigation menu), or a few sections that fit in tabs.
Instead:
- Navigation menu: Website navigation.
- Tabs: Views of one thing.
Variants
What each variant is for in any design system, then what Dense data decided and why. Each sample is the live story with that variant set.
defaultvariantUsed in Dense dataWhat it is for
Menu items with no border; hover shows the item.
In Dense data
Menu items with no border, quiet beside dense data.
outlinevariantNot used in Dense dataWhat it is for
Menu items with a border.
In Dense data
Borders on every item would compete with the data.
defaultsizeUsed in Dense dataWhat it is for
The normal row height.
In Dense data
Every section of the app.
smsizeUsed in Dense dataWhat it is for
A smaller row.
In Dense data
Long secondary lists, at the denser size.
lgsizeNot used in Dense dataWhat it is for
A taller row with room for an icon and two lines.
In Dense data
Rows stay small so the whole navigation fits the screen all shift.
Why it works this way
Sidebar looks the way it does because of 4 answers about this product. Each card is one answer, in three steps: the answer, what it decided for Sidebar, and what would move if it changed.
content.q11The answer
Most of the screen is numbers and records.
2So Sidebar
- ✓uses default: Menu items with no border, quiet beside dense data.
- –does not use outline: Borders on every item would compete with the data.
Badge: neutral Badge: info Badge: success Badge: warning Badge: danger Empty: default Item: default Item: outline Toggle: default Toggle: outline3If it changed
and 44 answers that follow from it: A lot, Scan and compare, People keep their place, and 41 more.
tasks.q21The answer
The same actions repeat many times an hour.
2So Sidebar
- ✓uses default: Every section of the app.
Button: ghost Input group: inline-end Alert: default Toast: success Toggle: default3If it changed
and 11 answers that follow from it: Time on task, All day, Three, and 8 more.
spacing.count1The answer
A second, denser size serves tables and toolbars.
2So Sidebar
- ✓uses sm: Long secondary lists, at the denser size.
Field: horizontal Item: default Item: sm3If it changed
No other answer follows from it.
content.q21The answer
Experts want many records on one screen.
2So Sidebar
- –does not use lg: Rows stay small so the whole navigation fits the screen all shift.
Button: default Button: compact Button: large Input group: block-start Input group: block-end Field: horizontal Toggle: default Toggle: sm Toggle: lg3If it changed
and 14 answers that follow from it: Quiet, Only the steps the screens need, Small, and 11 more.
Words
- Lead with the object, then the state: "Alert 2041 contained."
- Use numbers, not words for numbers.
- One verb per button. No "please", no exclamation marks.
- Use: Investigate, Filter, Assign, Escalated, Resolved. Avoid: Oops, Awesome, Please, Simply.
The labels components use, from the voice tokens:
content.action.commit: Savecontent.action.cancel: Cancelcontent.action.destroy: Deletecontent.action.retry: Retrycontent.action.empty-action: Change filters
See Voice and tone for the whole voice.
Accessibility and keyboard
Test before you ship:
- It is a navigation landmark; the current section is marked; collapsing keeps names for icons. undefined
Use it in code
npx open-ds add sidebar
The component reads only component keys and semantic tokens, so it follows the brand and mode on <html>.
Tokens it reads
Sidebar reads 113 tokens of its own, each an alias of a semantic token. Each row is one part: its own token, the semantic it aliases, and the value it ends at. Change a semantic and every component that reads it follows; override one of Sidebar's tokens to change Sidebar alone.
Dense data decides what changes when you switch theme. Here a switch changes colors only, so the marked rows differ between light and dark; everything else holds steady.
| Part | Its token and the semantic it aliases | Value |
|---|---|---|
| Wrapper | ||
| Fill Changes with theme | component.sidebar.wrapper.backgroundcolor.surface.defaultvar(--sidebar-wrapper-background) | Lightslate.light.0:#FFFFFFDarkslate.dark.50:#161B23 |
| Sidebar | ||
| Fill Changes with theme | component.sidebar.backgroundcolor.surface.defaultvar(--sidebar-background) | Lightslate.light.0:#FFFFFFDarkslate.dark.50:#161B23 |
| Text Changes with theme | component.sidebar.labelcolor.text.primaryvar(--sidebar-label) | Lightslate.light.900:#161B23Darkslate.dark.900:#F7F8FA |
| Padding | component.sidebar.paddingcomponent-spacing-map.x2var(--sidebar-padding) | space.8:0.5rem |
| Background hover Changes with theme on hover | component.sidebar.background-hovercolor.surface.defaultvar(--sidebar-background-hover) | Lightslate.light.0:#FFFFFFDarkslate.dark.50:#161B23 |
| Corner | component.sidebar.radiusradius.controlvar(--sidebar-radius) | radius.2:0.125rem |
| Text size | component.sidebar.font-sizetype.body.font-sizevar(--sidebar-font-size) | font.size.14:0.875rem |
| Focus ring Changes with theme | component.sidebar.focus-ringcolor.border.focusvar(--sidebar-focus-ring) | Lightviolet.light.600:#4B3FD9Darkviolet.dark.600:#A9A3F6 |
| Padding right | component.sidebar.padding-rightcomponent-spacing-map.x8var(--sidebar-padding-right) | space.32:2rem |
| Size | component.sidebar.sizesize.control.defaultvar(--sidebar-size) | size.32:2rem |
| Background hover 2 Changes with theme on hover | component.sidebar.background-hover-2color.surface.sunkenvar(--sidebar-background-hover-2) | Lightslate.light.100:#EEF1F5Darkslate.dark.100:#262D38 |
| Label hover Changes with theme on hover | component.sidebar.label-hovercolor.text.primaryvar(--sidebar-label-hover) | Lightslate.light.900:#161B23Darkslate.dark.900:#F7F8FA |
| Background active Changes with theme | component.sidebar.background-activecolor.surface.sunkenvar(--sidebar-background-active) | Lightslate.light.100:#EEF1F5Darkslate.dark.100:#262D38 |
| Label active Changes with theme | component.sidebar.label-activecolor.text.primaryvar(--sidebar-label-active) | Lightslate.light.900:#161B23Darkslate.dark.900:#F7F8FA |
| Disabled opacity disabled when disabled | component.sidebar.disabled-opacity-disabledopacity.disabledvar(--sidebar-disabled-opacity-disabled) | opacity.40:0.4 |
| Background 2 Changes with theme | component.sidebar.background-2color.surface.sunkenvar(--sidebar-background-2) | Lightslate.light.100:#EEF1F5Darkslate.dark.100:#262D38 |
| Weight | component.sidebar.font-weighttype.label.font-weightvar(--sidebar-font-weight) | font.weight.500:500 |
| Focus | component.sidebar.focusborder-width.focusvar(--sidebar-focus) | border-width.2:2px |
| Background open hover Changes with theme when open, on hover | component.sidebar.background-open-hovercolor.surface.sunkenvar(--sidebar-background-open-hover) | Lightslate.light.100:#EEF1F5Darkslate.dark.100:#262D38 |
| Label open hover Changes with theme when open, on hover | component.sidebar.label-open-hovercolor.text.primaryvar(--sidebar-label-open-hover) | Lightslate.light.900:#161B23Darkslate.dark.900:#F7F8FA |
| Icon size | component.sidebar.icon-sizesize.iconvar(--sidebar-icon-size) | size.16:1rem |
| Focus ring focus Changes with theme on focus | component.sidebar.focus-ring-focuscolor.border.focusvar(--sidebar-focus-ring-focus) | Lightviolet.light.600:#4B3FD9Darkviolet.dark.600:#A9A3F6 |
| Focus ring offset focus Changes with theme on focus | component.sidebar.focus-ring-offset-focuscolor.surface.canvasvar(--sidebar-focus-ring-offset-focus) | Lightslate.light.50:#F7F8FADarkslate.dark.0:#0D1117 |
| Height | component.sidebar.heightsize.iconvar(--sidebar-height) | size.16:1rem |
| Font size 2 | component.sidebar.font-size-2type.caption.font-sizevar(--sidebar-font-size-2) | font.size.12:0.75rem |
| Gap | ||
| Speed | component.sidebar.gap.durationmotion.duration.feedbackvar(--sidebar-gap-duration) | duration.fast:120ms |
| Value | component.sidebar.gapspace.gap.defaultvar(--sidebar-gap) | space.8:0.5rem |
| Container | ||
| Speed | component.sidebar.container.durationmotion.duration.feedbackvar(--sidebar-container-duration) | duration.fast:120ms |
| Inner | ||
| Fill Changes with theme | component.sidebar.inner.backgroundcolor.surface.defaultvar(--sidebar-inner-background) | Lightslate.light.0:#FFFFFFDarkslate.dark.50:#161B23 |
| Corner | component.sidebar.inner.radiusradius.containervar(--sidebar-inner-radius) | radius.4:0.25rem |
| Border Changes with theme | component.sidebar.inner.bordercolor.border.defaultvar(--sidebar-inner-border) | Lightslate.light.200:#DDE2EADarkslate.dark.100:#262D38 |
| Shadow | component.sidebar.inner.shadowelevation.cardvar(--sidebar-inner-shadow) | shadow.none |
| Rail | ||
| Background hover Changes with theme on hover | component.sidebar.rail.background-hovercolor.border.defaultvar(--sidebar-rail-background-hover) | Lightslate.light.200:#DDE2EADarkslate.dark.100:#262D38 |
| Inset | ||
| Fill Changes with theme | component.sidebar.inset.backgroundcolor.surface.canvasvar(--sidebar-inset-background) | Lightslate.light.50:#F7F8FADarkslate.dark.0:#0D1117 |
| Corner | component.sidebar.inset.radiusradius.containervar(--sidebar-inset-radius) | radius.4:0.25rem |
| Shadow | component.sidebar.inset.shadowelevation.cardvar(--sidebar-inset-shadow) | shadow.none |
| Input | ||
| Height | component.sidebar.input.heightsize.control.defaultvar(--sidebar-input-height) | size.32:2rem |
| Fill Changes with theme | component.sidebar.input.backgroundcolor.surface.canvasvar(--sidebar-input-background) | Lightslate.light.50:#F7F8FADarkslate.dark.0:#0D1117 |
| Header | ||
| Gap | component.sidebar.header.gapspace.gap.defaultvar(--sidebar-header-gap) | space.8:0.5rem |
| Padding | component.sidebar.header.paddingcomponent-spacing-map.x2var(--sidebar-header-padding) | space.8:0.5rem |
| Footer | ||
| Gap | component.sidebar.footer.gapspace.gap.defaultvar(--sidebar-footer-gap) | space.8:0.5rem |
| Padding | component.sidebar.footer.paddingcomponent-spacing-map.x2var(--sidebar-footer-padding) | space.8:0.5rem |
| Separator | ||
| Fill Changes with theme | component.sidebar.separator.backgroundcolor.border.defaultvar(--sidebar-separator-background) | Lightslate.light.200:#DDE2EADarkslate.dark.100:#262D38 |
| Content | ||
| Gap | component.sidebar.content.gapspace.gap.defaultvar(--sidebar-content-gap) | space.8:0.5rem |
| Group | ||
| Padding | component.sidebar.group.paddingcomponent-spacing-map.x2var(--sidebar-group-padding) | space.8:0.5rem |
| Label height | component.sidebar.group.label.heightsize.control.defaultvar(--sidebar-group-label-height) | size.32:2rem |
| Label radius | component.sidebar.group.label.radiusradius.controlvar(--sidebar-group-label-radius) | radius.2:0.125rem |
| Label padding x | component.sidebar.group.label.padding-xcomponent-spacing-map.x2var(--sidebar-group-label-padding-x) | space.8:0.5rem |
| Label font size | component.sidebar.group.label.font-sizetype.caption.font-sizevar(--sidebar-group-label-font-size) | font.size.12:0.75rem |
| Label font weight | component.sidebar.group.label.font-weighttype.label.font-weightvar(--sidebar-group-label-font-weight) | font.weight.500:500 |
| Label label Changes with theme | component.sidebar.group.label.labelcolor.text.primaryvar(--sidebar-group-label-label) | Lightslate.light.900:#161B23Darkslate.dark.900:#F7F8FA |
| Label focus ring Changes with theme | component.sidebar.group.label.focus-ringcolor.border.focusvar(--sidebar-group-label-focus-ring) | Lightviolet.light.600:#4B3FD9Darkviolet.dark.600:#A9A3F6 |
| Label duration | component.sidebar.group.label.durationmotion.duration.feedbackvar(--sidebar-group-label-duration) | duration.fast:120ms |
| Label icon size | component.sidebar.group.label.icon-sizesize.iconvar(--sidebar-group-label-icon-size) | size.16:1rem |
| Label focus on focus | component.sidebar.group.label.focusborder-width.focusvar(--sidebar-group-label-focus) | border-width.2:2px |
| Label focus ring focus Changes with theme on focus | component.sidebar.group.label.focus-ring-focuscolor.border.focusvar(--sidebar-group-label-focus-ring-focus) | Lightviolet.light.600:#4B3FD9Darkviolet.dark.600:#A9A3F6 |
| Label focus ring offset focus Changes with theme on focus | component.sidebar.group.label.focus-ring-offset-focuscolor.surface.canvasvar(--sidebar-group-label-focus-ring-offset-focus) | Lightslate.light.50:#F7F8FADarkslate.dark.0:#0D1117 |
| Action radius | component.sidebar.group.action.radiusradius.controlvar(--sidebar-group-action-radius) | radius.2:0.125rem |
| Action label Changes with theme | component.sidebar.group.action.labelcolor.text.primaryvar(--sidebar-group-action-label) | Lightslate.light.900:#161B23Darkslate.dark.900:#F7F8FA |
| Action focus ring Changes with theme | component.sidebar.group.action.focus-ringcolor.border.focusvar(--sidebar-group-action-focus-ring) | Lightviolet.light.600:#4B3FD9Darkviolet.dark.600:#A9A3F6 |
| Action background hover Changes with theme on hover | component.sidebar.group.action.background-hovercolor.surface.sunkenvar(--sidebar-group-action-background-hover) | Lightslate.light.100:#EEF1F5Darkslate.dark.100:#262D38 |
| Action label hover Changes with theme on hover | component.sidebar.group.action.label-hovercolor.text.primaryvar(--sidebar-group-action-label-hover) | Lightslate.light.900:#161B23Darkslate.dark.900:#F7F8FA |
| Action icon size | component.sidebar.group.action.icon-sizesize.iconvar(--sidebar-group-action-icon-size) | size.16:1rem |
| Action focus on focus | component.sidebar.group.action.focusborder-width.focusvar(--sidebar-group-action-focus) | border-width.2:2px |
| Action focus ring focus Changes with theme on focus | component.sidebar.group.action.focus-ring-focuscolor.border.focusvar(--sidebar-group-action-focus-ring-focus) | Lightviolet.light.600:#4B3FD9Darkviolet.dark.600:#A9A3F6 |
| Action focus ring offset focus Changes with theme on focus | component.sidebar.group.action.focus-ring-offset-focuscolor.surface.canvasvar(--sidebar-group-action-focus-ring-offset-focus) | Lightslate.light.50:#F7F8FADarkslate.dark.0:#0D1117 |
| Content font size | component.sidebar.group.content.font-sizetype.body.font-sizevar(--sidebar-group-content-font-size) | font.size.14:0.875rem |
| Menu | ||
| Gap | component.sidebar.menu.gapspace.gap.tightvar(--sidebar-menu-gap) | space.4:0.25rem |
| Action radius | component.sidebar.menu.action.radiusradius.controlvar(--sidebar-menu-action-radius) | radius.2:0.125rem |
| Action label Changes with theme | component.sidebar.menu.action.labelcolor.text.primaryvar(--sidebar-menu-action-label) | Lightslate.light.900:#161B23Darkslate.dark.900:#F7F8FA |
| Action focus ring Changes with theme | component.sidebar.menu.action.focus-ringcolor.border.focusvar(--sidebar-menu-action-focus-ring) | Lightviolet.light.600:#4B3FD9Darkviolet.dark.600:#A9A3F6 |
| Action background hover Changes with theme on hover | component.sidebar.menu.action.background-hovercolor.surface.sunkenvar(--sidebar-menu-action-background-hover) | Lightslate.light.100:#EEF1F5Darkslate.dark.100:#262D38 |
| Action label hover Changes with theme on hover | component.sidebar.menu.action.label-hovercolor.text.primaryvar(--sidebar-menu-action-label-hover) | Lightslate.light.900:#161B23Darkslate.dark.900:#F7F8FA |
| Action icon size | component.sidebar.menu.action.icon-sizesize.iconvar(--sidebar-menu-action-icon-size) | size.16:1rem |
| Action focus on focus | component.sidebar.menu.action.focusborder-width.focusvar(--sidebar-menu-action-focus) | border-width.2:2px |
| Action focus ring focus Changes with theme on focus | component.sidebar.menu.action.focus-ring-focuscolor.border.focusvar(--sidebar-menu-action-focus-ring-focus) | Lightviolet.light.600:#4B3FD9Darkviolet.dark.600:#A9A3F6 |
| Action focus ring offset focus Changes with theme on focus | component.sidebar.menu.action.focus-ring-offset-focuscolor.surface.canvasvar(--sidebar-menu-action-focus-ring-offset-focus) | Lightslate.light.50:#F7F8FADarkslate.dark.0:#0D1117 |
| Badge radius | component.sidebar.menu.badge.radiusradius.controlvar(--sidebar-menu-badge-radius) | radius.2:0.125rem |
| Badge padding x | component.sidebar.menu.badge.padding-xcomponent-spacing-map.x1var(--sidebar-menu-badge-padding-x) | space.4:0.25rem |
| Badge font size | component.sidebar.menu.badge.font-sizetype.caption.font-sizevar(--sidebar-menu-badge-font-size) | font.size.12:0.75rem |
| Badge font weight | component.sidebar.menu.badge.font-weighttype.label.font-weightvar(--sidebar-menu-badge-font-weight) | font.weight.500:500 |
| Badge label Changes with theme | component.sidebar.menu.badge.labelcolor.text.primaryvar(--sidebar-menu-badge-label) | Lightslate.light.900:#161B23Darkslate.dark.900:#F7F8FA |
| Skeleton height | component.sidebar.menu.skeleton.heightsize.control.defaultvar(--sidebar-menu-skeleton-height) | size.32:2rem |
| Skeleton gap | component.sidebar.menu.skeleton.gapspace.gap.defaultvar(--sidebar-menu-skeleton-gap) | space.8:0.5rem |
| Skeleton radius | component.sidebar.menu.skeleton.radiusradius.controlvar(--sidebar-menu-skeleton-radius) | radius.2:0.125rem |
| Skeleton padding x | component.sidebar.menu.skeleton.padding-xcomponent-spacing-map.x2var(--sidebar-menu-skeleton-padding-x) | space.8:0.5rem |
| Sub gap | component.sidebar.menu.sub.gapspace.gap.tightvar(--sidebar-menu-sub-gap) | space.4:0.25rem |
| Sub border Changes with theme | component.sidebar.menu.sub.bordercolor.border.defaultvar(--sidebar-menu-sub-border) | Lightslate.light.200:#DDE2EADarkslate.dark.100:#262D38 |
| Sub padding x | component.sidebar.menu.sub.padding-xcomponent-spacing-map.x3var(--sidebar-menu-sub-padding-x) | space.12:0.75rem |
| Sub padding y | component.sidebar.menu.sub.padding-ycomponent-spacing-map.hairlinevar(--sidebar-menu-sub-padding-y) | space.2:0.125rem |
| Sub button gap | component.sidebar.menu.sub.button.gapspace.gap.defaultvar(--sidebar-menu-sub-button-gap) | space.8:0.5rem |
| Sub button radius | component.sidebar.menu.sub.button.radiusradius.controlvar(--sidebar-menu-sub-button-radius) | radius.2:0.125rem |
| Sub button padding x | component.sidebar.menu.sub.button.padding-xcomponent-spacing-map.x2var(--sidebar-menu-sub-button-padding-x) | space.8:0.5rem |
| Sub button label Changes with theme | component.sidebar.menu.sub.button.labelcolor.text.primaryvar(--sidebar-menu-sub-button-label) | Lightslate.light.900:#161B23Darkslate.dark.900:#F7F8FA |
| Sub button focus ring Changes with theme | component.sidebar.menu.sub.button.focus-ringcolor.border.focusvar(--sidebar-menu-sub-button-focus-ring) | Lightviolet.light.600:#4B3FD9Darkviolet.dark.600:#A9A3F6 |
| Sub button background hover Changes with theme on hover | component.sidebar.menu.sub.button.background-hovercolor.surface.sunkenvar(--sidebar-menu-sub-button-background-hover) | Lightslate.light.100:#EEF1F5Darkslate.dark.100:#262D38 |
| Sub button label hover Changes with theme on hover | component.sidebar.menu.sub.button.label-hovercolor.text.primaryvar(--sidebar-menu-sub-button-label-hover) | Lightslate.light.900:#161B23Darkslate.dark.900:#F7F8FA |
| Sub button background active Changes with theme | component.sidebar.menu.sub.button.background-activecolor.surface.sunkenvar(--sidebar-menu-sub-button-background-active) | Lightslate.light.100:#EEF1F5Darkslate.dark.100:#262D38 |
| Sub button label active Changes with theme | component.sidebar.menu.sub.button.label-activecolor.text.primaryvar(--sidebar-menu-sub-button-label-active) | Lightslate.light.900:#161B23Darkslate.dark.900:#F7F8FA |
| Sub button disabled opacity disabled when disabled | component.sidebar.menu.sub.button.disabled-opacity-disabledopacity.disabledvar(--sidebar-menu-sub-button-disabled-opacity-disabled) | opacity.40:0.4 |
| Sub button icon size | component.sidebar.menu.sub.button.icon-sizesize.iconvar(--sidebar-menu-sub-button-icon-size) | size.16:1rem |
| Sub button focus on focus | component.sidebar.menu.sub.button.focusborder-width.focusvar(--sidebar-menu-sub-button-focus) | border-width.2:2px |
| Sub button focus ring focus Changes with theme on focus | component.sidebar.menu.sub.button.focus-ring-focuscolor.border.focusvar(--sidebar-menu-sub-button-focus-ring-focus) | Lightviolet.light.600:#4B3FD9Darkviolet.dark.600:#A9A3F6 |
| Sub button focus ring offset focus Changes with theme on focus | component.sidebar.menu.sub.button.focus-ring-offset-focuscolor.surface.canvasvar(--sidebar-menu-sub-button-focus-ring-offset-focus) | Lightslate.light.50:#F7F8FADarkslate.dark.0:#0D1117 |
| Default | ||
| Background hover Changes with theme | component.sidebar.default.background-hovercolor.surface.sunkenvar(--sidebar-default-background-hover) | Lightslate.light.100:#EEF1F5Darkslate.dark.100:#262D38 |
| Label hover Changes with theme on hover | component.sidebar.default.label-hovercolor.text.primaryvar(--sidebar-default-label-hover) | Lightslate.light.900:#161B23Darkslate.dark.900:#F7F8FA |
| Height | component.sidebar.default.heightsize.control.defaultvar(--sidebar-default-height) | size.32:2rem |
| Text size | component.sidebar.default.font-sizetype.body.font-sizevar(--sidebar-default-font-size) | font.size.14:0.875rem |
| Outline | ||
| Fill Changes with theme | component.sidebar.outline.backgroundcolor.surface.canvasvar(--sidebar-outline-background) | Lightslate.light.50:#F7F8FADarkslate.dark.0:#0D1117 |
| Background hover Changes with theme on hover | component.sidebar.outline.background-hovercolor.surface.sunkenvar(--sidebar-outline-background-hover) | Lightslate.light.100:#EEF1F5Darkslate.dark.100:#262D38 |
| Label hover Changes with theme on hover | component.sidebar.outline.label-hovercolor.text.primaryvar(--sidebar-outline-label-hover) | Lightslate.light.900:#161B23Darkslate.dark.900:#F7F8FA |
| Sm | ||
| Text size | component.sidebar.sm.font-sizetype.caption.font-sizevar(--sidebar-sm-font-size) | font.size.12:0.75rem |
| Lg | ||
| Text size | component.sidebar.lg.font-sizetype.body.font-sizevar(--sidebar-lg-font-size) | font.size.14:0.875rem |
| From the system | ||
| Edge Changes with theme | color.border.default straight from the system, through --border | Lightslate.light.200 · #DDE2EADarkslate.dark.100 · #262D38 |