ComponentsAgent patterns

Mention List

The panel that opens when you type @ or / in a message. The text field keeps focus and moves through the options with the arrow keys. Options can sit in groups, and status rows say when a source is loading, empty, or failed.

Files with @

Type @ in the message, or press Add file. With Google Drive connected, results sit in a Files group and a Google Drive group, and each source has its own status row. Without Drive the files are one flat list. Search, files, and Drive in this example are sample data.

Usage

Mention list usageTSX
import {
  MentionGroup,
  MentionList,
  MentionOption,
  MentionStatus,
} from "@tulipfarm/sprout/mention-list";

<Textarea
  aria-autocomplete="list"
  aria-controls={open ? listId : undefined}
  aria-activedescendant={open && options.length ? optionId(active) : undefined}
  onKeyDown={moveWithArrows}
/>

{open && (
  <MentionList
    id={listId}
    label="Files you can add"
    status={loading ? <MentionStatus tone="loading">Searching your files…</MentionStatus> : undefined}
    announcement={`${options.length} files you can add. Use Up and Down to choose, Enter to add, Escape to close.`}
  >
    <MentionGroup label="Files">
      {files.map((file, index) => (
        <MentionOption
          key={file.id}
          id={optionId(index)}
          active={index === active}
          onSelect={() => add(file)}
          media={<FileTypeIcon name={file.name} mediaType={file.mediaType} />}
          title={file.name}
          description="48 KB"
        />
      ))}
    </MentionGroup>
  </MentionList>
)}

MentionList takes the listbox id and a label. status holds one or more MentionStatus rows under the options. announcement is read aloud when it changes, because a textarea keeps its textbox role and cannot say that a list opened.

MentionOption needs an id for aria-activedescendant, active, onSelect, and a title. media (a 16px icon, file icon, brand glyph, or small avatar) and description are optional. Long titles end in an ellipsis and show in full on hover. The active option scrolls into view.

MentionStatus has three tones: loading adds a spinner,empty is muted text, and error is an alert in the danger color. Pass action for a small outline button such as Retry.

Skills with /

One flat list with a book icon and the skill version. Typing /c at the start also offers the /compact command. Picking a skill stages it above the message.

Rich editor panel

The rich text editor shares one panel for files and skills. An error wins over loading, and loading wins over the empty text. An option can have no icon or no description.

  • Launch brief.pdf1.2 MB
  • Retro notes
  • Roadmap 2026Google Docs
  • Imported row without an icon

4 options. Use Up and Down to choose, Enter to select, Escape to close.

Nothing selected yet.

States

Each source reports its own status, so rows can stack: results with a source still loading, or two failed sources where only one offers a retry.

    Loading skills…

    Loading
      No files yet. Upload one in Files to use it here.

      Empty

        Error with retry

          Error without action
          Searching Google Drive…

          2 files you can add. Use Up and Down to choose, Enter to add, Escape to close.

          Results and a loading source

            Two failed sources

            No retries yet.

            People

            Grouped people and agents with avatars. Click a row to select it.

            Alex Meadow selected

            Accessibility

            Focus stays in the text field. ↓ and ↑ move the active option and wrap across groups, Enter or Tab picks it, and Escape closes the panel. When a search failed, Enter retries. Options and the retry button ignore mouse focus, so the caret never leaves the message. Touch rows are 44px tall; mouse rows are 36px. The panel rises in over 120ms and row highlights fade over 120ms; reduced motion turns both off. In forced colors the active option gets an outline.