UIDex

Sidebar

الشريط الجانبي

<aside> / <nav>

also calledside navigation، side nav، nav sidebar، navigation rail، app shell navigation، side menu، سايدبار، القائمة الجانبية، شريط التنقّل الجانبي، اللوح الجانبي، العمود الجانبي، قائمة لوحة التحكّم

A sidebar is a permanent column along the inline edge of the page that carries app navigation, or content that supports the screen you are on. It is a piece of the layout rather than a layer over it: it shares the same grid as the content, so 240px of sidebar takes exactly 240px off the content area. Nothing about it needs a scrim or a focus trap, and Escape has nothing to close, because the most a user can do is collapse it. It is not a drawer: a drawer is a temporary surface that slides over the content and dismisses on an outside click or on Escape, while a sidebar stays visible with nobody doing anything. A navigation rail is this same column narrowed to icons with the labels hidden, one DOM structure in a second state, even in the systems that ship the rail as a component of its own. And it is not a navbar, which runs horizontally across the page and runs out of room after five or six links, where a sidebar holds dozens of them in groups because a vertical axis can scroll. The practical test: if showing it pushes the content over, it is a sidebar; if it covers the content, it is a drawer.

If you called it…

"the column of links down the side of the app""the menu that is on every page in the same place""the part that shrinks to just icons when you click the arrow""the dashboard menu with the highlighted page""the tall list of icons next to the content"

Live specimen

Interact with the demo. Every part is real and numbered.

لوحة التحكّمشريط كامل

الشريط يقاسم هذه المنطقة الصفّ نفسه. طيّه يوسّع المحتوى ولا يكشف طبقة تحته.

RTLالعمود عند اليمين في العربية، وسهم الطيّ وحده هو ما ينعكس

Anatomy: every part, named

Hover a row to locate it in the demo above.

Build prompt

Build a permanent sidebar for a dashboard app. Make it a column inside the page layout itself with grid-template-columns: var(--sidebar-w) 1fr rather than a layer over the content, so there is no scrim, no focus trap, and no Escape handler. Wrap the links in a <nav> carrying an aria-label that separates it from any other <nav> on the page, and use <aside> instead when what it holds is supporting content rather than links. Arrange the links in groups, each group a <ul> tied to its heading with aria-labelledby, and any heading that collapses its group is a button with aria-expanded. Every nav item fills its row through padding and holds an aria-hidden="true" icon plus a text label. Mark the current page with aria-current="page" on the link itself and on exactly one item, never with colour alone. Add a collapse button with aria-expanded and a stable name that excludes the state, and persist the choice in localStorage. In the collapsed state keep the text of every label in the DOM inside a visually hidden span, or put an aria-label on the link, and do not let a tooltip stand in for the accessible name. Let the link list alone scroll with overflow-y: auto and min-block-size: 0, keep the header, the collapse button, and the account footer outside the scroll area, and size the height with 100dvh rather than 100vh. Use logical properties everywhere: border-inline-end for the separator, padding-inline-start for nested indentation, inset-inline-start for pinning, margin-inline-start for offsetting the content, and never left, right, margin-left, or padding-left. Drive the collapse from inline-size, or from a column width variable registered with @property, instead of translateX. Mirror the collapse chevron alone in RTL through a dedicated class, and never add a blanket rule that flips every svg.

How it behaves right-to-left

This section is ours alone.

Which edge it sits on

mirrors

A sidebar on the left in English sits on the right in Arabic, and it moves there by itself when the layout is a grid or a flex row and the column comes first in DOM order, because the inline axis follows dir. Do not write grid-template-columns: 240px 1fr and then reverse it by hand under [dir="rtl"]: the same value is correct both ways as long as the first track is the logical start. What actually breaks is position: fixed with left: 0 and the content pushed over with margin-left: 240px, where nothing mirrors at all, so write inset-inline-start: 0 and margin-inline-start instead.

The border against the content

mirrors

The line that separates the column from the content is drawn on the edge facing the content, which is border-inline-end and not border-right; with border-right the line jumps to the outer window edge in Arabic and the seam is left bare. The scrollbar of the link list takes that same edge, since a vertical scrollbar sits at the inline end of its box, so it crosses from the right side to the left side when direction flips and a padding-right meant to hold the labels clear of it is left behind: write padding-inline-end, or reserve the room with scrollbar-gutter: stable. If you draw the seam as a shadow instead, box-shadow has no logical form at all, so multiply its horizontal offset by a direction variable to make it mirror.

Indenting nested items

mirrors

Child items inside a group are indented away from the start edge, which in Arabic means to the right, and the indent is padding-inline-start: 2rem on the list item, never padding-left and never margin-left. Do not fake it with blank spaces or a positive text-indent: the spaces land inside the link text and get read out with it, and text-indent moves only the first line, so the shape breaks the moment a label wraps. The vertical guide line that marks the nesting belongs at inset-inline-start: 0 on the group, drawn with a pseudo-element so it travels to the other side with everything else.

The chevron and the hamburger

mirrors

The collapse chevron is a directional icon: it points at the edge the column folds toward, so it mirrors with the page. The hamburger is three horizontal bars symmetric about the vertical axis, so scaleX(-1) buys nothing while the bars are equal in length and quietly reverses the design as soon as they are not. The working rule: never write [dir="rtl"] svg { transform: scaleX(-1) }, put a dedicated class on chevrons and back arrows only, and leave the section icons (home, user, settings, bell) alone.

The collapse animation

never mirrors

Collapsing is a change of width, not a change of place, and a width has no side to mirror. Doing it with transform: translateX(-190px) gets two things wrong at once: the column leaves by the wrong edge in Arabic, since transforms have no logical form and the offset has to be multiplied by a direction variable, and a gap its full width stays behind in the layout, because a transform never touches the space an element occupies. Animate inline-size instead, or move the transition onto grid-template-columns while both track lists keep the same number of tracks so the values in between can be computed, since a raw --sidebar-w custom property jumps from one value to the other unless you register it with @property and a <length> syntax.

In code

Each row is one framework's word for the same thing. Take the row your project speaks.

HTML<nav aria-label="…"> / <aside>nav for links and aside for supporting content, with the name separating it from the other navigation landmarks.
ARIAaria-current="page"The only marker that announces the current page, since colour reaches nobody.
ARIAaria-expanded="true|false"Carries the collapse state so the name of the button never has to say it.
CSSborder-inline-end + padding-inline-startThe seam against the content and the nested indentation, both mirroring with no extra rule.
shadcn/ui<SidebarProvider> + useSidebar()Ships the collapsed and rail states ready made, and stores the choice in a cookie so it survives a reload.
Material UI<Drawer variant="permanent">Material has no component called Sidebar, so the permanent sidebar is a variant of Drawer.
Ant Design<Layout.Sider collapsible collapsedWidth={80}>Collapsing and the rail width are ready props, but persisting the choice between pages is still on you.

See also

Updated · 2026-08-19