Skip to content

Sidebar

<aparte-sidebar>

The column beside the chat — conversations, a new-chat button, a search field, an account row — as an element, because it has behaviour: it collapses, it becomes a drawer on a narrow window, and its search field filters the conversation list.

What it does NOT own is its content. Children are yours, in the order you want them: a .aparte-sidebar__header, a .aparte-sidebar__search, a .aparte-sidebar__body holding the <aparte-conversation-list>, a .aparte-sidebar__footer. The stylesheet lays those out and draws nothing you did not put there — the recipe (.aparte-sidebar, in styles/shell/sidebar.css) is the look, this element is the three behaviours.

Collapse. collapsed is an attribute, reflected, so the host can set it, read it and persist it. Any element carrying data-aparte-sidebar-toggle anywhere on the page toggles the nearest sidebar (or the one whose id the attribute names), so a hamburger in the header needs no script — and the sidebar keeps aria-expanded and aria-controls on that control in step, whoever changed the state (it gives itself an id when the host wrote none). aparte-sidebar-toggle fires on every change, whoever caused it.

Drawer. Under 48rem of window the sidebar leaves the flow and slides over the page (data-drawer, set by the element from a media query); open, it draws a scrim, moves the focus to its first focusable child, closes on Escape from anywhere on the page or on a click outside, and hands the focus back to the control that opened it. Nothing here is a portal: the drawer is the same element in the same place, positioned fixed, which is all a sidebar needs and what a dialog would not get away with. Collapsed — folded as a column or slid off as a drawer — it carries inert and aria-hidden, so nothing invisible keeps a tab stop.

Search. An input carrying data-aparte-sidebar-search filters the conversation list below it by title as the user types — rows that do not match are hidden, and a date group with nothing left hides with them. Client-side, on the titles the list already has: an index over message bodies is the storage adapter’s business.

Width Open in a tab
The real element, in its own document so this site's CSS cannot reach it. It renders the same example printed below — every framework mounts this same element, so the rendering is what you get in all five; the code is what changes.
<!-- The sidebar alone, as a column: its header, a search field that filters the list,
the list, a footer. The shell it sits in — header, chat — is the app-shell guide's.
breakpoint="none" keeps it a column at any width; without it, under 48rem of
window it becomes a drawer behind a [data-aparte-sidebar-toggle] control. -->
<aparte-sidebar breakpoint="none" style="height: 22rem">
<div class="aparte-sidebar__header">
<span class="aparte-sidebar__brand">aparté</span>
<button class="aparte-btn aparte-btn--icon aparte-btn--sm" type="button" aria-label="New chat">
<aparte-icon name="edit"></aparte-icon>
</button>
</div>
<div class="aparte-sidebar__search aparte-field-group">
<input class="aparte-field aparte-field--sm" type="search" placeholder="Search conversations" data-aparte-sidebar-search>
</div>
<div class="aparte-sidebar__body">
<aparte-conversation-list active-id="c1"></aparte-conversation-list>
</div>
<div class="aparte-sidebar__footer">
<span class="aparte-avatar aparte-avatar--sm">P</span> Paul
</div>
</aparte-sidebar>
<script>
const day = 864e5;
document.querySelector('aparte-conversation-list').conversations = [
{ id: 'c1', title: 'Deploy checklist', updatedAt: Date.now() },
{ id: 'c2', title: 'Rename the segment types', updatedAt: Date.now() - day },
{ id: 'c3', title: 'Tokens, not selectors', updatedAt: Date.now() - 4 * day },
{ id: 'c4', title: 'The first release', updatedAt: Date.now() - 60 * day },
];
</script>
AttributeDescription
collapsedHidden (in the flow) or closed (as a drawer). Reflected; set it to start closed.
breakpointThe window width under which the sidebar becomes a drawer: a length (default 48rem), or none for a column that never does.
data-drawerReflected BY the element while the window is narrower than breakpoint (48rem unless you set it). Read-only.
PropertyTypeDescription
collapsedbooleanWhether the sidebar is collapsed (hidden in the flow, or closed as a drawer).
drawer (readonly)booleanTrue while the window is under the breakpoint and the sidebar is a drawer.
MethodDescription
open(opener?: HTMLElement): voidOpen the sidebar. opener is the control to hand the focus back to when it closes.
close(): void
toggle(opener?: HTMLElement): void
filter(query: string): voidHide the conversation rows whose title does not contain query, and the date groups left empty. Case- and accent-insensitive. An empty query shows everything.
EventTypeDescription
aparte-sidebar-toggleCustomEvent&lt;AparteSidebarToggleDetail>The sidebar opened or closed, by a toggle, by Escape, by a click on the scrim or by collapsed being set. Bubbles.

Override any of these on :root, on a subtree, or on one instance — custom properties inherit downward. Some are this element’s own; others are site-wide tokens that also style it, and overriding one of those at :root moves everything that reads it. The full set is in the CSS variables reference.

VariableDefaultDescription
--aparte-sidebar-width260pxWidth of the column, and of the drawer.
--aparte-sidebar-insetvar(--aparte-space-3)The inline inset of the four regions (header, search, body, footer): one value, so their content stands on one axis.
--aparte-sidebar-bgvar(--aparte-surface-2)Its ground.

The element is the same object everywhere — the tag does not change. What changes is how an attribute is written and how an event reaches you.

<aparte-sidebar collapsed=""></aparte-sidebar>
el.addEventListener('aparte-sidebar-toggle', (e) => use(e.detail));

Installation and the framework-specific traps: React · Vue · Svelte · Angular.