Responsive Shell Sidebar pattern

Resize this viewport: at ≤900px the leading + trailing sidebars collapse to drawers triggered by the toolbar buttons. Above 900px the sidebars are inline. The same content is mounted in both surfaces; consumer signals drive both copies via the parent template's re-read.

The pattern

Three pieces:

  1. Inline mounts for the sidebars wrapped in .wide-only (display: contents at >900px; display: none at ≤900px).
  2. Drawer twins with the same children, mounted outside the shell's named slots, controlled by toolbar buttons inside .narrow-only.
  3. MediaQueryList listener that auto-closes the drawers on viewport widening (the gap FEEDBACK-06 §3 explicitly flagged).

Dual-mount caveat

Components with local DOM state (uncontrolled form inputs, scroll position, focused element) will lose state when the viewport crosses the breakpoint — the inline copy unmounts and the drawer copy mounts (or vice versa). Signal-bound components are safe because their state lives in the signal, not the DOM. Most AdiaUI primitives are signal-backed; the affected cases are:

  • Native <input>/<textarea> with no signal binding
  • Components using this.querySelector(...) with imperative state
  • Scroll-position-sensitive containers (e.g. chat threads at a specific scroll)

For these, either: (a) lift the state to a signal in the parent, or (b) document the unmount-on-resize edge.

Auto-close on viewport widening

const mql = window.matchMedia('(max-width: 900px)'); mql.addEventListener('change', (e) => { if (!e.matches) { // Viewport widened past breakpoint — close any open drawers document.getElementById('leading-drawer').open = false; document.getElementById('trailing-drawer').open = false; } });

State sharing

Single signal source consumed by both copies. The parent template reads the signal once and passes the extracted value to both the inline mount AND the drawer mount; on signal change the parent re-renders both:

import { signal } from '@adia-ai/web-components/core'; const activeToken = signal('primary'); static template = () => { const current = activeToken.value; // dep read here return html` <aside class="wide-only"> <list-ui> ... .active=${current === 'primary'} ... </list-ui> </aside> <drawer-ui id="leading-drawer"> <list-ui> ... .active=${current === 'primary'} ... </list-ui> </drawer-ui> `; }

Future v0.6.0: built-in responsive-breakpoint

FEEDBACK-06 §3 Option A proposed a built-in <editor-sidebar responsive-breakpoint="900"> attribute that auto-stamps the drawer twin. Deferred to v0.6.0 — requires editor-shell + pane-ui semantics audit (focus handoff at the inline↔drawer transition, dual-mount event-listener deduplication, etc.). Pairs with a <show-when breakpoint="..."> trait. Until then, the pattern above is the canonical recipe.

Tokens

Primary Secondary Neutral Surface

Settings

Lightness Chroma Hue