Overlays

ChatComposerPopover

Chat composer-bound slash-command and mention listbox primitive.

import { ChatComposerPopover } from '@lostgradient/chat/composer-popover';
chatcommandoverlay
01

Overview

Chat composer-bound slash-command and mention listbox primitive.

Usage

svelte
<script lang="ts">
  import ChatComposerPopover from '@lostgradient/chat/composer-popover';
  import { ChatInput } from '@lostgradient/chat';

  const commands = [
    { value: 'help', label: 'Help', description: 'Show available commands' },
    { value: 'new', label: 'New conversation', description: 'Start over' },
  ];

  let value = $state('');
</script>

<ChatComposerPopover id="composer-commands" bind:value items={commands}>
  {#snippet composer(composerProps)}
    <ChatInput
      id="chat"
      bind:value
      composerRole={composerProps.composerRole}
      composerAriaExpanded={composerProps.composerAriaExpanded}
      composerAriaControls={composerProps.composerAriaControls}
      composerAriaActiveDescendant={composerProps.composerAriaActiveDescendant}
      composerAriaAutocomplete={composerProps.composerAriaAutocomplete}
      oncomposerinput={composerProps.oncomposerinput}
      oncomposerkeydown={composerProps.oncomposerkeydown}
      oncomposerselectionchange={composerProps.oncomposerselectionchange}
      oncomposerblur={composerProps.oncomposerblur}
    />
  {/snippet}
</ChatComposerPopover>

The default placement is top-start, which keeps suggestions above a composer anchored near the bottom of the viewport. Pass placement when a different starting side is appropriate; the underlying floating overlay still flips and shifts the menu when available space requires it.

When the composer is the full Chat surface, commit a selection with the public range API. insertAtRange() updates the popover's bound value through oncomposerinput, so no synthetic DOM event is needed:

svelte
<script lang="ts">
  import { Chat, createConversation } from '@lostgradient/chat';
  import ChatComposerPopover from '@lostgradient/chat/composer-popover';

  const conversation = createConversation({ id: 'assistant' });
  const commands = [
    { value: 'help', label: 'Help', insert: '/help ' },
    { value: 'new', label: 'New conversation', insert: '/new ' },
  ];
  let chat: ReturnType<typeof Chat> | undefined;
  let value = $state('');
</script>

<ChatComposerPopover
  id="composer-commands"
  bind:value
  items={commands}
  onSelect={(selection) => chat?.insertAtRange(selection.range, selection.item.insert)}
>
  {#snippet composer(composerProps)}
    <Chat bind:this={chat} id="assistant-chat" {conversation} {...composerProps} />
  {/snippet}
</ChatComposerPopover>

Serializing entity mentions

Keep the composer as a plain textarea by committing a selected item as a Markdown link. serializeChatComposerMention() escapes labels and entity URIs, while parseChatComposerMentions() restores the submitted text projection and its UTF-16 mention ranges:

ts
import {
  parseChatComposerMentions,
  serializeChatComposerMention,
  type ChatComposerPopoverSelection,
} from '@lostgradient/chat/composer-popover';

type MentionItem = {
  value: string;
  label: string;
};

function commitMention(selection: ChatComposerPopoverSelection<MentionItem>): void {
  const mention = serializeChatComposerMention({
    label: selection.item.label,
    uri: selection.item.value,
  });

  chat?.insertAtRange(selection.range, mention);
}

function submitComposer(value: string): void {
  const { text, mentions } = parseChatComposerMentions(value);
  sendMessage({ text, mentions });
}

Only absolute non-web URI schemes such as person: and linear: become mentions. Ordinary https:, mailto:, relative, image, and malformed links remain literal textarea text. The helpers are also available from the @lostgradient/chat package root.

Related

  • Chat — full conversation surface and composer.
  • CommandMenu — generic caret-anchored command list.
  • CommandItem — selectable command row used by the popover.
Live preview

No messages yet

0 messages in conversation

Selected: None

02

When to use

Use when
  • Adding slash commands, mentions, or autocomplete to ChatInput without re-implementing combobox ARIA.
  • Composer suggestions should stay anchored to the active token and leave command definitions in application code.
Avoid when
  • Opening a global command launcher detached from the composer — use command-palette instead.
  • Anchoring a generic command menu to an arbitrary input — use command-menu instead.
03

Examples

Slash commands in Chat

Wires ChatComposerPopover to Chat and commits selections through the public composer API.

04

Props

Props for chat-composer-popover
Name
Type
Default
Description
id required text Unique identifier used for the listbox and item ids.
value bindable text '' Current composer value. Keep this synchronized through ChatInput binding or Chat's oncomposerinput.
items TItem[] [] Consumer-owned command or mention definitions.Immediate, ungrouped suggestions. Default [].
sources ChatComposerPopoverSource<TItem>[] [] Typed synchronous or asynchronous grouped suggestion sources. Default [].
triggers string[] ["/","@"] Trigger characters that open the popover. Default ['/', '@'].
label text 'Composer suggestions' Accessible listbox label. Default 'Composer suggestions'.
placement 'top''bottom''left''right''top-start''top-end''bottom-start''bottom-end' 'top-start' Caret-relative placement. Default 'top-start', which keeps the menu above a bottom composer.
offset number 6 Distance in px between the caret and popover. Default 6.
composer required snippet Render the ChatInput or compatible composer with the provided overlay props.
item snippet Optional custom row contents. Defaults to the item label and description.
empty snippet Optional empty state rendered when filtering produces no matching rows.
detectTrigger (value: string, selectionStart: number, selectionEnd: number, ) => ChatComposerPopoverTriggerMatchnull Override trigger detection.
filter (items: readonly TItem[], query: string, trigger: string) => readonly TItem[] Override filtering.
onSelect (selection: ChatComposerPopoverSelection<TItem>) => void Invoked when an enabled item is selected by keyboard or pointer.
onDismiss () => void Invoked when Escape, trigger loss, or outside pointerdown dismisses the popover.
05

Accessibility

ArrowUp/ ArrowDown
Moves the active suggestion.
Enter
Selects the active suggestion.
Tab
Selects the active suggestion and keeps focus in the composer.
Escape
Dismisses the suggestion popover.
Passes combobox role and aria-expanded, aria-controls, aria-activedescendant, and aria-autocomplete through to ChatInput's composer overlay API.