Skip to content
kitn AI/UI

Conversations

kai-conversations

A self-contained sidebar that renders a grouped, scrollable conversation list with a “New chat” button and collapse toggle — drop it into any framework and wire up three events.

  • Shadow DOM
  • 3 events
  • Flat or grouped lists
  • Built-in search
  • Declarative child elements

Set the data in JavaScript (it’s arrays and objects) and listen for CustomEvents:

<kai-conversations id="sidebar" style="display:block; width:300px; height:100vh;"></kai-conversations>
<script type="module">
import '@kitn.ai/ui/elements';
await customElements.whenDefined('kai-conversations');
const el = document.getElementById('sidebar');
el.conversations = [
{
id: 'c-1', title: 'Web component architecture',
scope: { type: 'collection' }, messageCount: 12,
lastMessageAt: '2026-06-16T10:00:00.000Z',
updatedAt: '2026-06-16T10:00:00.000Z',
},
];
el.activeId = 'c-1';
el.addEventListener('kai-conversation-select', (e) => {
el.activeId = e.detail.id; // keep the active highlight in sync
});
el.addEventListener('kai-new-chat', () => console.log('new chat'));
el.addEventListener('kai-toggle-sidebar', () => console.log('toggled'));
</script>

Two data approaches:

  • In JavaScriptel.conversations is the rows; el.groups is the section headers. They work together, not as alternatives: each row lands in the group whose id matches its groupId, and anything with no match falls into a single Ungrouped section. Leave groups empty and the whole list renders as one Ungrouped section.
  • Declarative children — nest <kai-conversation> tags directly; each carries its id as an attribute and title as text content. The element reads them on mount and watches via MutationObserver — works in plain HTML, SSR, or any framework that sets innerHTML.
Child elementAttributesText contentNotes
group-idid
YesParse a single light-DOM `<kai-conversation>` element into a `ConversationSummary`. Attribute mapping: - `id` → ConversationSummary.id - `group-id` → ConversationSummary.groupId (optional) - textContent → ConversationSummary.title Required fields not expressible as HTML attributes (`scope`, `messageCount`, `lastMessageAt`, `updatedAt`) receive safe defaults so the rendered list item is fully functional with just `id` + title text.

A flat conversations array with no groups — every row renders in one Ungrouped section.

activeId highlights the matching row and scrolls it into view.

Supply groups for the section headers and give each conversation a matching groupId; anything unmatched falls to the “Ungrouped” section.

An empty conversations array renders the built-in empty state.

Nested <kai-conversation> elements — no JavaScript wiring needed.

Replace the title bar or the empty state, or add a footer row (account, settings, usage). The slot names mirror <kai-chat>, so the two compose with one vocabulary.

SlotModePurpose
replaceFull custom title bar; replaces the built-in toggle / "Chats" / New-chat row.
replaceCustom zero-state shown when there are no conversations; replaces the built-in "No conversations yet".
injectA row below the list: account, settings, or usage.
PropertyTypeDefaultNotes
theme'auto'Color mode (`auto` follows prefers-color-scheme).
groups[]The list's section headers (`{ id, name, sortOrder, createdAt }`), rendered in array order. A group carries no conversations of its own; it is matched against `conversations` by id, so the two props are complementary rather than alternatives. Omit for an ungrouped list. Set as a JS property.
conversations[]Every conversation the list renders, flat. Each one is filed under the group whose `id` equals its `groupId`; one with no `groupId`, or with a `groupId` matching no entry in `groups`, falls into a trailing "Ungrouped" section, so nothing you pass in is ever dropped. There is no recency bucketing. Set as a JS property. Omit to supply them as `<kai-conversation>` light-DOM children instead, or for the empty state.
activeIdThe id of the currently-open conversation, highlighted in the list.
collapsedControlled collapsed state. Set as a JS property (`el.collapsed = true`) to drive the rail from your app, updating it in response to `kai-collapse-toggle`. Omit for uncontrolled (the element manages it). Collapsed shrinks the rail to a floating reopen button.
defaultCollapsedInitial collapsed state when uncontrolled (default false). Use the `default-collapsed` attribute to start collapsed in plain HTML.
EventDetailNotes
The rail was collapsed or expanded (via the toggle, the reopen button, or a `collapse()`/`expand()`/`toggle()` call).
A conversation was selected.
Record<string, never> The "New chat" button was clicked.
The built-in search box query changed (typing, or a programmatic `clear()` which fires it with `''`). Lets a consumer mirror or server-side the filter.
Record<string, never> The sidebar toggle was clicked.
MethodSignatureNotes
(options?: FocusOptions): voidFocus the built-in search input inside the shadow root.
(): voidClear the internal search query (resets the list filter) and fire kai-search with an empty string.
(id: string): voidProgrammatically select a conversation by id. The mirror of the kai-conversation-select event (a convenience over driving `activeId`).
(): voidCollapse the rail to its floating reopen button (fires `kai-collapse-toggle`).
(): voidExpand the rail back to the full list (fires `kai-collapse-toggle`).
(): voidToggle the rail collapsed/expanded (fires `kai-collapse-toggle`).

This element wraps these SolidJS components — reach for them directly when you need finer control than the props expose.

ConversationListCollapsedRail