Conversation list
<aparte-conversation-list>
Class: AparteConversationList
Conversation-history sidebar — a framework-agnostic web component. The host sets
the conversations JS property and the active-id attribute; this renders the
rows and fires the user’s intent. Acting on it is the host’s, unless manage hands
the row menu’s writes to the registered conversation manager.
A row is two real buttons: the title, which selects, and a ⋯ that opens the row’s
menu — rename, pin or unpin, archive or unarchive, delete. Every item fires an event
and stops; the one exception is delete, which asks first, inline in the menu,
because it is the one action the host cannot undo. Rename swaps the title for an
input: Enter or leaving the field commits, Escape cancels, and an unchanged or
emptied title fires nothing. The rows are grouped by date — Pinned, Today,
Yesterday, Previous 7 days, Previous 30 days, then one heading per month — as soon
as any item carries updatedAt; set flat to render them flat.
Children are not a composition point: _render() assigns innerHTML from the
conversations array, so any light-DOM child a host writes inside the element is
discarded the next time the list renders — and switching this element’s locale is
enough to trigger one. Compose around the element, not inside it: it renders rows
and nothing else, with no header, no new-conversation button and no search field.
What it is not: a store. Selecting, renaming, pinning, archiving and deleting all
leave the array untouched — the events carry an id (and, for rename, the title) and
stop. Set manage and the menu’s write gestures are carried out on the conversation
manager you registered, so those six listeners are no longer yours to write; the
event still goes out first and preventDefault() takes the gesture back. The array
is still not this element’s: the manager’s own change is what re-renders it.
A row’s text comes from the array, so it changes when the host assigns
conversations again; the exception is an empty title, which falls back to the
locale’s new-chat label and therefore follows a locale switch. An archived item is
still rendered (it gains aparte-conv-item--archived); filtering archived
conversations out of the list is the host’s decision, not this element’s. The
asymmetry between the two inputs is deliberate: active-id is an attribute because
moving the selection patches the rendered rows in place, while conversations is a
JS property because it is structured data an attribute cannot carry, and setting it
re-renders the whole list.
Example
Section titled “Example”<!-- It stores nothing and fetches nothing: an empty tag renders the empty state, and the list appears when the host assigns `conversations`. --><aparte-conversation-list active-id="c1" style="max-width: 20rem"></aparte-conversation-list>
<script> const day = 864e5; document.querySelector('aparte-conversation-list').conversations = [ { id: 'c0', title: 'Release checklist', updatedAt: Date.now() - 3 * day, pinnedAt: Date.now() }, { 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 }, ];</script>// The host owns the data: set the `conversations` property, listen for the intent.const list = document.querySelector('aparte-conversation-list')!;list.conversations = [ { id: 'c1', title: 'Deploy checklist', updatedAt: Date.now() }, { id: 'c2', title: 'Old thread', updatedAt: 0, archivedAt: Date.now() },];list.setAttribute('active-id', 'c1');
list.addEventListener('aparte-conversation-select', (e) => load(e.detail.id));list.addEventListener('aparte-conversation-rename', (e) => manager.updateTitle(e.detail.id, e.detail.title));list.addEventListener('aparte-conversation-pin', (e) => manager.pin(e.detail.id));list.addEventListener('aparte-conversation-delete', (e) => manager.delete(e.detail.id));// Or let the list finish the gesture: register the manager once, add `manage`, and// rename / pin / archive / delete are carried out for you. The events still fire.aparteGlobalConfig.setConversationManager(manager);list.setAttribute('manage', '');Attributes
Section titled “Attributes”| Attribute | Description |
|---|---|
active-id | The id of the conversation to render as selected. |
flat | Render the rows flat, in host order, with no date headings. |
loading | The list is on its way: six skeleton rows stand where the rows will be, the element is aria-busy and a visually hidden line names the wait. A list that is not there yet is not an empty list. Set it while your store answers, clear it when you assign conversations. |
manage | Let the list finish the gesture: rename, pin, unpin, archive, unarchive and delete are performed on the conversation manager registered with setConversationManager(), instead of leaving you six listeners to write. The event still fires first, and preventDefault() on it takes the gesture back. Off by default, and selecting stays yours either way. |
Properties
Section titled “Properties”| Property | Type | Description |
|---|---|---|
loading | boolean | The list is on its way: rows of the kit’s skeleton stand where the rows will be, the element is aria-busy, and a visually hidden line names the wait. Reflected as the loading attribute, so a site sets it while its store answers and clears it when conversations is set. A list that is not there yet is not an empty list. |
manage | boolean | Let the list finish the gesture through the registered conversation manager. Reflected as the manage attribute. The property is not a nicety here: this is the attribute that authorises a WRITE, and Vue and both Sveltes stringify a bound false onto an element that has no property of that name — manage="false", which hasAttribute reads as on. So turning management off turned it on, and a delete the host meant to handle itself was also done by the list. presenceOn, like loading above it. |
conversations | AparteConversationListItem[] | Set the list of conversations to display. Triggers a re-render. |
Events
Section titled “Events”| Event | Type | Description |
|---|---|---|
aparte-conversation-select | CustomEvent<AparteConversationSelectDetail> | A row’s title was activated; the host loads that conversation. |
aparte-conversation-rename | CustomEvent<AparteConversationRenameDetail> | A rename was committed with a new, non-empty title. Cancelable: preventDefault() takes the gesture back from manage. |
aparte-conversation-pin | CustomEvent<AparteConversationPinDetail> | The pin item was chosen on an unpinned row. Cancelable: preventDefault() takes the gesture back from manage. |
aparte-conversation-unpin | CustomEvent<AparteConversationPinDetail> | The same item on a pinned row; same detail shape, opposite intent. Cancelable, like the rest. |
aparte-conversation-archive | CustomEvent<AparteConversationArchiveDetail> | The archive item was chosen on a live conversation. Cancelable: preventDefault() takes the gesture back from manage. |
aparte-conversation-unarchive | CustomEvent<AparteConversationArchiveDetail> | The same item on an already-archived one; same detail shape, opposite intent. Cancelable, like the rest. |
aparte-conversation-delete | CustomEvent<AparteConversationDeleteDetail> | The delete was confirmed. Cancelable: preventDefault() takes the gesture back from manage. |
Theming
Section titled “Theming”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.
<aparte-conversation-list>
Section titled “<aparte-conversation-list>”| Variable | Default | Description |
|---|---|---|
--aparte-conv-list-gap | var(--aparte-space-1) | Vertical gap between rows, and between a group’s heading and its rows. |
--aparte-conv-item-padding | var(--aparte-space-4) var(--aparte-space-5) | Padding of a row’s title button. |
--aparte-conv-item-gap | var(--aparte-space-3) | Gap between a row’s title and its ⋯ button. |
--aparte-conv-item-radius | var(--aparte-radius-md) | Corner radius of a row. |
--aparte-conv-item-font-size | var(--aparte-font-size-md) | Font size of a row’s title. |
--aparte-conv-item-color | var(--aparte-text) | Title colour of an inactive row. |
--aparte-conv-item-bg-hover | var(--aparte-surface-3) | Row background on hover. |
--aparte-conv-item-bg-active | var(--aparte-surface-3) | Background of the row matching active-id. |
--aparte-conv-item-color-active | var(--aparte-text) | Title colour of the active row. |
--aparte-conv-item-font-weight-active | var(--aparte-font-weight-medium) | Title weight of the active row. |
--aparte-conv-action-btn-size | var(--aparte-btn-size-sm) | Square size of the ⋯ button (the recipe’s small step). Under (pointer: coarse) the stylesheet redeclares it as the touch target size on the button itself, so a value set on the element does not reach it there; the button also stays visible instead of appearing on hover. |
In a framework
Section titled “In a framework”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-conversation-list flat=""></aparte-conversation-list>el.addEventListener('aparte-conversation-select', (e) => use(e.detail));<aparte-conversation-list flat=""></aparte-conversation-list>The aparte-* tags are typed JSX intrinsics as soon as you import from @aparte/react. A presence attribute takes '', never true — React stringifies it, and flat={false} would render flat="false", which hasAttribute reads as on. Events reach you by ref, typed through the DOM.
<template> <aparte-conversation-list flat="" @aparte-conversation-select="(e) => use(e.detail)" ></aparte-conversation-list></template>Declared through Vue’s GlobalComponents, so vue-tsc checks the tag in any template. A presence attribute takes '' to set and null to remove, never false.
<aparte-conversation-list flat="" on:aparte-conversation-select={(e) => use(e.detail)}></aparte-conversation-list>Declared through SvelteHTMLElements, so svelte-check covers the attributes and the on: handlers. A presence attribute takes '', never false.
import { AparteConversationListDirective } from '@aparte/angular';<aparte-conversation-list [flat]="true" (conversationSelect)="use($event)"></aparte-conversation-list>A standalone directive whose selector IS the tag, so the real element sits in the template — @if, @for and content projection all reach it — and no CUSTOM_ELEMENTS_SCHEMA is needed.
Installation and the framework-specific traps: React · Vue · Svelte · Angular.