Sidebar
stimeo--sidebar
A responsive collapsible sidebar: an inline rail that becomes a modal off-canvas panel below a breakpoint.
The stimeo--sidebar controller has no dedicated APG pattern: the base is Disclosure (the trigger's aria-expanded controls the panel) and, below the breakpoint, it borrows the Dialog (Modal) focus behavior via the shared FocusTrap (the same trap used by dialog / alert-dialog / drawer). Above the breakpoint it is an inline, non-modal rail that toggles expanded/collapsed and persists that preference in localStorage. Below it, it becomes an overlay off-canvas panel: opening moves focus in, traps Tab, locks background scroll, makes the background inert, and Escape or a backdrop click closes it and restores focus. role="dialog" is intentionally not applied, so the landmark stays an aside/nav. Narrow the demo below 600px to see the overlay mode. Behavior only — the rail width, slide, and backdrop are this Playground's CSS.
Keyboard
| Key | Action |
|---|---|
| Esc | Close the overlay (below the breakpoint) and return focus to the trigger. |
| Tab / Shift+Tab | Cycle focus within the panel while the overlay is open (focus trap). |
<%# Markup for the sidebar (collapsible sidebar) demo.
This demo focuses on the INLINE rail: the toggle collapses / expands it and the
preference persists in localStorage across reloads. (The same sidebar can also
switch to a modal off-canvas panel below a breakpoint — that document-level modal
mode is described in the text and is the same off-canvas pattern the Drawer demo
shows; it is not staged here because a page-level modal inside a catalog card reads
as taking over the page.) breakpoint=0 keeps this demo always inline. The library
syncs aria-expanded, data-mode, and data-state; the rail width / animation are
demo.css's. role="dialog" is intentionally not applied — the panel stays a nav. %>
<div
class="sidebar-demo"
data-controller="stimeo--sidebar"
data-stimeo--sidebar-breakpoint-value="0"
data-stimeo--sidebar-key-value="catalog-demo">
<div class="sidebar-demo__bar">
<button
type="button"
class="sidebar-demo__toggle"
data-stimeo--sidebar-target="trigger"
data-action="click->stimeo--sidebar#toggle"
aria-expanded="true"
aria-controls="sidebar-demo-panel">
<%= t("components.sidebar.demo.toggle") %>
</button>
<span class="sidebar-demo__brand"><%= t("components.sidebar.demo.brand") %></span>
</div>
<div class="sidebar-demo__body">
<nav
id="sidebar-demo-panel"
class="sidebar-demo__panel"
data-stimeo--sidebar-target="panel"
aria-label="<%= t('components.sidebar.demo.nav_label') %>"
data-mode="inline"
data-state="expanded">
<% t("components.sidebar.demo.items").each do |label| %>
<a class="sidebar-demo__link" href="#"><%= label %></a>
<% end %>
</nav>
<main class="sidebar-demo__content">
<p><%= t("components.sidebar.demo.hint") %></p>
</main>
</div>
</div>
/*
* Presentation-only styles for the sidebar demo (inline rail).
* Stimeo ships behavior only: the library syncs aria-expanded, data-mode, and
* data-state (expanded/collapsed). This demo shows the inline rail — the toggle
* animates the rail's width via data-state. (The overlay/modal off-canvas mode the
* component also supports is described in the text, not staged here; see the Drawer
* demo for the same off-canvas pattern.)
*/
.sidebar-demo {
overflow: hidden;
border: 1px solid var(--border-strong);
border-radius: 0.5rem;
background: var(--surface-card);
}
.sidebar-demo__bar {
display: flex;
align-items: center;
gap: 0.75rem;
padding: 0.6rem 0.9rem;
border-bottom: 1px solid var(--border-default);
background: var(--surface-subtle);
}
.sidebar-demo__toggle {
padding: 0.35rem 0.7rem;
font: inherit;
cursor: pointer;
border: 1px solid var(--border-interactive);
border-radius: 0.375rem;
background: var(--surface-card);
}
.sidebar-demo__brand {
font-weight: 600;
color: var(--color-text);
}
.sidebar-demo__body {
display: flex;
min-height: 12rem;
}
/* Panel: a rail whose width follows data-state. */
.sidebar-demo__panel {
display: flex;
flex-direction: column;
gap: 0.25rem;
width: 12rem;
padding: 0.75rem;
border-right: 1px solid var(--border-default);
background: var(--surface-card);
overflow: hidden;
transition: width 0.2s ease, padding 0.2s ease;
}
.sidebar-demo__panel[data-state="collapsed"] {
width: 0;
padding-inline: 0;
border-right-color: transparent;
}
/* Fully collapsed: hide the links so they leave the tab order too (a width:0 +
overflow:hidden rail would still keep them keyboard-focusable but invisible). */
.sidebar-demo__panel[data-state="collapsed"] .sidebar-demo__link {
display: none;
}
.sidebar-demo__link {
white-space: nowrap;
padding: 0.4rem 0.5rem;
border-radius: 0.375rem;
color: var(--color-text);
text-decoration: none;
}
.sidebar-demo__link:hover {
background: var(--surface-subtle);
}
.sidebar-demo__link:focus-visible {
outline: 2px solid var(--color-primary-hover);
outline-offset: 2px;
}
.sidebar-demo__content {
flex: 1;
padding: 1rem;
color: var(--color-text-muted);
}
@media (prefers-reduced-motion: reduce) {
.sidebar-demo__panel {
transition: none;
}
}
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--sidebar"
Targets
| Name | Description | Attribute |
|---|---|---|
trigger
|
The button toggling the panel; its aria-expanded reflects the expanded state. |
data-stimeo--sidebar-target="trigger" |
panel
|
The sidebar panel, keyed off data-mode (inline/overlay) and data-state. |
data-stimeo--sidebar-target="panel" |
backdrop
|
The overlay-mode backdrop, typically wired to close on click. | data-stimeo--sidebar-target="backdrop" |
Values
| Name | Description | Attribute |
|---|---|---|
breakpoint
|
The min-width px at or above which the sidebar is inline, below which it is an overlay (default 768). |
data-stimeo--sidebar-breakpoint-value |
key
|
The localStorage key persisting the collapsed preference; empty disables persistence (default empty). |
data-stimeo--sidebar-key-value |
collapsed
|
The declared initial inline collapsed state, used when no persisted/DOM state exists (default false). |
data-stimeo--sidebar-collapsed-value |
Actions
| Name | Description | Action |
|---|---|---|
close
|
Hides the panel (inline: collapse; overlay: close). | stimeo--sidebar#close |
open
|
Shows the panel (inline: expand; overlay: open). | stimeo--sidebar#open |
toggle
|
Toggles the panel (inline: collapsed/expanded; overlay: open/closed). | stimeo--sidebar#toggle |
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 |
|---|---|---|
aria-expanded |
Trigger | Whether the panel is expanded (inline) / open (overlay). |
data-mode |
Panel | inline (>= breakpoint) / overlay (< breakpoint). |
data-state |
Panel | expanded / collapsed (inline) or open / closed (overlay). |
inert |
Background siblings | Added outside the panel while the overlay is open. |
hidden |
Backdrop | Removed only while the overlay is open. |