Drawer
stimeo--drawer
A modal panel sliding in from a screen edge: open state for CSS, exit animation played in full.
A modal panel that slides in from a screen edge. Keeping focus inside, stopping background scrolling, holding the background inert and restoring focus work as they do in the plain dialog. On top of that the open and closed state is published for CSS, so the slide in and out can be written in CSS alone. The panel is hidden only after the closing transition finishes, so the exit animation is never cut short. Which edge it comes from is set in the markup, never computed. Esc and a click on the backdrop close it. Position, slide direction and easing are your CSS.
Keyboard
| Key | Action |
|---|---|
| Esc | Close the drawer and return focus to the opener. |
| Tab / Shift+Tab | Cycle focus within the panel (focus trap). |
<%# Markup for the drawer (side panel) demo.
A modal panel that slides in from the right edge. In addition to focus move/trap,
Esc, background scroll lock, background inert, and returning focus to the trigger,
the library syncs data-state (open/closed) and applies hidden once the transition
finishes. Which edge it moves from and how (the slide) and the interpretation of
placement are expressed by the consumer's CSS (demo.css) reading data-placement /
data-state. %>
<div
class="drawer"
data-controller="stimeo--drawer"
data-stimeo--drawer-placement-value="right">
<button
type="button"
class="demo-trigger"
data-stimeo--drawer-target="trigger"
data-action="click->stimeo--drawer#open">
<%= t("components.drawer.demo.open_button") %>
</button>
<%# Overlay (backdrop). Click to close. data-state stays in sync with the panel. %>
<div
class="drawer__overlay"
data-stimeo--drawer-target="overlay"
data-action="click->stimeo--drawer#closeOnBackdrop"
hidden>
<%# The role="dialog" + aria-modal panel. Starts closed / hidden. %>
<div
class="drawer__panel"
role="dialog"
aria-modal="true"
aria-labelledby="drawer-title"
data-stimeo--drawer-target="panel"
data-state="closed"
hidden>
<h3 id="drawer-title" class="drawer__title">
<%= t("components.drawer.demo.title") %>
</h3>
<p><%= t("components.drawer.demo.body") %></p>
<button
type="button"
class="demo-trigger drawer__close"
data-action="click->stimeo--drawer#close">
<%= t("components.drawer.demo.close") %>
</button>
</div>
</div>
</div>
/* Overlay: a full-screen semi-transparent backdrop that fades in/out. */
.drawer__overlay {
position: fixed;
inset: 0;
background: rgb(0 0 0 / 0.5);
opacity: 1;
transition: opacity 0.2s ease;
z-index: 50;
}
.drawer__overlay[data-state="closed"] {
opacity: 0;
}
/* Keep it fully removed while hidden is set (honor the library's hidden control). */
.drawer__overlay[hidden] {
display: none;
}
/* Panel: fixed to the right edge; data-state drives the slide transition. */
.drawer__panel {
position: fixed;
top: 0;
bottom: 0;
width: min(20rem, 80vw);
padding: 1.5rem;
background: var(--surface-card);
box-shadow: -8px 0 30px rgb(15 23 42 / 0.2);
transition: transform 0.25s ease;
overflow: auto;
}
.drawer__panel[hidden] {
display: none;
}
/* placement=right: enters/exits from the right edge; parks off-screen when closed. */
.drawer__panel[data-placement="right"] {
right: 0;
transform: translateX(0);
}
.drawer__panel[data-placement="right"][data-state="closed"] {
transform: translateX(100%);
}
.drawer__title {
margin: 0 0 0.5rem;
font-size: 1.125rem;
}
.drawer__close {
margin-top: 1.5rem;
}
This demo needs no consumer-side JS (the controller handles the behavior).
These demo styles use shared design tokens (light + dark). Copy the shared styles too, then toggle data-theme on your root element for dark mode.
The data-* attributes you add to your own HTML to wire this component. Put the data-controller below on a root element, then place its targets / values / actions inside that element.
On the root element
data-controller="stimeo--drawer"
Targets
| Name | Description | Attribute |
|---|---|---|
trigger
|
The button that opens the drawer; focus falls back here on close. | data-stimeo--drawer-target="trigger" |
overlay
|
The backdrop wrapping the panel; syncs data-state and closes the drawer when clicked directly. |
data-stimeo--drawer-target="overlay" |
panel
required
|
The role="dialog" aria-modal panel that slides in; carries data-state/data-placement and is hidden only after the exit transition. |
data-stimeo--drawer-target="panel" |
Values
| Name | Description | Attribute |
|---|---|---|
placement
|
The edge the drawer slides from, reflected as data-placement for CSS (default right). |
data-stimeo--drawer-placement-value |
open
|
Whether the drawer is open; reflects/initializes the open state (default false). | data-stimeo--drawer-open-value |
Actions
| Name | Description | Action |
|---|---|---|
close
|
Sets data-state to closed to start the exit slide, then defers hidden and modal teardown until the transition ends. |
stimeo--drawer#close |
closeOnBackdrop
|
Closes only when the overlay itself (not its contents) is clicked. | stimeo--drawer#closeOnBackdrop |
open
|
Reveals the panel/overlay, flips data-state to open to play the enter slide, traps focus, and locks background scroll. |
stimeo--drawer#open |
State hooks
The library only manages these ARIA/data attributes and custom properties. Your CSS reads them to render the look — selectors like [aria-selected], [aria-expanded], or var(--stimeo--…) hook into this state.
| Hook | Target | Meaning |
|---|---|---|
data-state |
Panel / Overlay | open / closed; the start point for the slide transition. |
hidden |
Panel / Overlay | Applied after the close transition completes (fully hidden). |
data-placement |
Panel | left / right / top / bottom; a CSS flag for which edge to slide from. |
inert |
Background siblings | Added to elements outside the panel while open. |