Drawer / Side Panel
الدرج الجانبيrole="dialog" / inset-inline-start
also calledside panel، side sheet، navigation drawer، slide-out panel، off-canvas menu، flyout panel، درور، سايد بانل، اللوحة الجانبية المنزلقة، قائمة جانبية منزلقة، درج التنقّل، ورقة جانبية
A drawer is a temporary surface that slides in from one edge of the viewport, stays welded to that edge, and runs along its full length until it is dismissed. What defines it is an anchor edge paired with a context that stays visible: the user can still see the page they came from, which is why drawers are the right home for navigation, filters, and detail panels. It is not a modal dialog, because a dialog floats in the middle of the viewport, belongs to no edge, and exists to extract a decision before you continue. It is not a sidebar either: a sidebar is permanent page furniture that takes part in the layout and pushes content aside rather than covering it, and it closes with neither Escape nor an outside click, because it does not close at all. Nor is it a bottom sheet, which is the mobile counterpart, anchored to the bottom edge and travelling along the block axis instead of the inline one. Modality is not part of the definition: a drawer may be modal, with a scrim, a focus trap, and a scroll lock, or non-modal, leaving everything behind it clickable and Tab-reachable, so blocking is an axis entirely independent of anchoring. The practical test: if the user cannot dismiss it and carry on with what they were doing, what you built is a dialog in the shape of a drawer.
If you called it…
Live specimen
Interact with the demo. Every part is real and numbered.
التنقّل
اسحبه نحو حافّته، أو Esc
Anatomy: every part, named
Hover a row to locate it in the demo above.
Build prompt
How it behaves right-to-left
This section is ours alone.
The anchor edge itself flips
mirrorsThis is the flip everyone forgets: what mirrors is not the contents of the drawer but the edge it hangs from. A navigation drawer that comes out of the left of the screen in English has to come out of the right in Arabic, because it is bound to where reading starts, not to physical left. Write it as inset-inline-start: 0 with inset-block: 0 and an explicit inline-size, delete left and width entirely, and turn the divider between panel and content into border-inline-end instead of border-right. The one legitimate exception is a drawer tied to a fixed meaning rather than to reading, such as a detail panel that always opens at the end of a table, and even that is written inset-inline-end, never right.
The slide needs a direction variable
mirrorstranslateX has no logical counterpart: the translate function works on the physical X axis whatever the page direction, and there is no translate-inline in the spec. So a hard-coded translateX(-100%) makes the Arabic drawer start off-screen on the side opposite its own edge and fly across the whole viewport before it lands. Define --dir: 1 on the root and --dir: -1 under [dir="rtl"], then write transform: translateX(calc(-100% * var(--dir))). The alternative is animating inset-inline-start from -320px to 0, which mirrors for free but forces layout every frame, whereas transform runs on the compositor.
The swipe-to-close direction reverses
mirrorsPointer and touch coordinates are always physical: clientX grows toward the right of the screen in Arabic exactly as in English, and the browser never flips it for you under RTL. So a condition like if (deltaX < -60) close() is an outright bug, not a matter of taste: in Arabic a start-anchored drawer sits on the right, so the swipe that puts it away travels right and yields a positive deltaX that never satisfies the test and the panel just snaps back, while the one gesture that does satisfy it, a drag leftward, away from the anchor edge and into the page , closes the drawer by hauling it across the content, the exact opposite of what the hand is doing. Read the direction at event time with const sign = getComputedStyle(panel).direction === "rtl" ? -1 : 1, and multiply the delta by it before any comparison. The same fix is owed to a velocityX threshold and to ArrowLeft / ArrowRight if you bind them.
The close button and the shadow
mirrorsThe two move together to the opposite side. The × sits at the logical end of the panel header: make it the last child of a flex row with justify-content: space-between and it settles by itself, or pin it with inset-inline-end instead of right. The shadow is the quieter trap: box-shadow has no logical form in CSS at all, and a value like 8px 0 24px throws the shadow off the outer edge of the viewport after the flip, where the browser clips it, leaving the panel with no lift over the content it is meant to sit above. Write the horizontal offset as calc(8px * var(--dir)), or zero it and let the blur carry it alone, since blur is symmetric by nature. The × glyph is symmetric about its vertical axis so it is never flipped, while a collapse chevron is a directional icon and does mirror with it.
What does not mirror: block axis and blocking
never mirrorsPage direction governs the inline axis only. A drawer anchored to a block edge, the bottom for instance, never moves between the two languages: inset-block-end: 0 is the same bottom in both, and its deltaY drag axis keeps the same sign, so it needs no direction variable at all. The scrim is neutral too: inset: 0 covers everything and contains nothing that could flip. Scroll lock behind the drawer is neutral as well, since overflow: hidden and overscroll-behavior: contain have nothing to do with direction, and exactly one thing inside it does flip: the vanished scrollbar sits at the logical inline-end edge, which is the left of a dir="rtl" page, so compensate with scrollbar-gutter: stable rather than a hard-coded padding-right.
In code
Each row is one framework's word for the same thing. Take the row your project speaks.
See also
Updated · 2026-08-19