Skip to content

MentionPicker

<caos-mention-picker>

the shortlist of people and records offered while somebody types an @mention

May be placed on a record page, an app’s home page and a page inside an app.

Name Attribute Description Type Default Required
candidates property only Who or what may be chosen, in the order they should read. Drawn exactly as given — this piece never filters or re-orders, because whoever fetched them applied the access rules and a second opinion here would disagree with theirs. Each entry is { kind, ref, label, hint? }. Setting them puts the highlight back on the first row. json empty list Optional
emptyLabel empty-label, markup only What is said when nothing matched. Worth setting: the default says a true thing about a list and nothing about what the reader could try instead. text No matches. Optional
Name When it fires What it carries
choose Somebody clicked a row, or the surface called chooseActive because they pressed Enter. Nothing has been written into any text box — turning the choice into a mention is the surface’s job, and insertMention is the function that does it. { kind, ref, label, hint? }
active-change The highlighted row changed. The id is what a composer puts in its own aria-activedescendant, which is how a screen reader follows a highlight that is moving in an element the reader is not focused on. { index: number, id: string

Nothing goes inside this piece.

Design token What it controls
--caos-color-surface Behind the list, so it reads as floating over the page.
--caos-color-border The list’s edge.
--caos-shadow-float The lift that separates the list from what is under it.
--caos-color-text The names.
--caos-color-text-subtle The second line that tells two identical names apart.
--caos-color-text-muted The line shown when nothing matched.
--caos-color-accent-fill Behind the highlighted row.
--caos-color-accent-contrast The text drawn on that fill.
--caos-font The typeface of the list.
--caos-text-row The size of a name.
--caos-text-label The size of the second line.
--caos-space-1 The inset around the rows.
--caos-space-2 Inside a row, and the gap between a name and its second line.
--caos-radius-sm The roundness of the list and of the highlighted row.
Name Description Arguments
moveDown() Move the highlight down, wrapping at the bottom. What a composer calls when the reader presses the down arrow; this element hears no keys of its own. not stated
moveUp() Move the highlight up, wrapping at the top. not stated
chooseActive() Choose the highlighted row, raising choose. Returns what was chosen, or null when there was nothing — so a composer can tell “Enter chose somebody” from “Enter should send”. not stated
Name Type What it tells you
activeIndex number Which row Enter would choose. Always within the list; 0 when the list is empty.
active the highlighted candidate, or null What Enter would choose. Null when there is nothing to choose.
activeOptionId text, or null The id of the highlighted row, for the composer’s aria-activedescendant. Null when there is nothing highlighted.
Part Which piece of it When it is there
::part(list) the floating list itself Always
::part(option) one choosable row Always
::part(label) the name in a row Always
::part(hint) the second line in a row that candidate was given one
::part(empty) the line shown when nothing matched there are no candidates

Somebody is typing a mention into prose and needs to say who they meant without leaving the box. The list is short, it is driven by the keys they are already pressing, and it disappears when the mention is finished.

  • LookupField — A record is being CHOSEN AS A VALUE rather than named inside a sentence. That is a field with a label, a stored id and a save behind it, none of which a mention has.
  • OverflowMenu — The list is a set of actions to invoke rather than candidates to name. A menu takes focus and owns its keyboard, which is the opposite of what this does.
  • PicklistField — The choice is one of a declared list of values rather than a person or a record. That list is fixed and known in advance; this one is fetched for every keystroke.

It is a listbox that the text box beside it OWNS — the combobox pattern. It never takes focus and has no keyboard handler: the composer keeps focus, forwards the arrows through moveUp/moveDown, and points at the highlighted row by putting activeOptionId into its own aria-activedescendant. Rows carry role="option" and aria-selected, so the highlight is announced as a selection rather than only drawn. Clicking a row is handled on mousedown with the default prevented, which is what stops the click from blurring the text box and losing the caret the insertion needs. The “nothing matched” line is role="presentation" rather than an option, because announcing it as a row tells a reader there is something to pick when there is not. Left to the author: naming the list, with aria-label — it is named “Mention suggestions” by default, which is right for one list on a page and wrong for two.

The second line is what tells two people with the same name apart.

Cannot be shown as a page placement: is given aria-label, which is set on the element rather than declared as an input. A stored placement carries only a component’s own inputs, so a page copying this would place the piece without it.

<caos-mention-picker id="mentionPicker1" aria-label="Mention suggestions"></caos-mention-picker>
// What an attribute cannot hold, set as a property — the same values the preview is drawn with.
const mentionPicker1 = document.getElementById('mentionPicker1');
mentionPicker1.candidates = [
{
"kind": "person",
"ref": "3f4c1b2a-5d6e-4f70-8a91-b2c3d4e5f607",
"label": "Morgan Hale",
"hint": "morgan.hale@example.test"
},
{
"kind": "person",
"ref": "8b7a6c5d-4e3f-4210-9876-543210fedcba",
"label": "Morgan Iyer",
"hint": "m.iyer@example.test"
},
{
"kind": "person",
"ref": "1a2b3c4d-5e6f-4708-9a0b-1c2d3e4f5061",
"label": "Morgana Reyes",
"hint": "morgana@example.test"
}
];

A surface merges what the people lookup returned with what a record search returned; this piece draws the merged list in the order it was handed.

Cannot be shown as a page placement: is given aria-label, which is set on the element rather than declared as an input. A stored placement carries only a component’s own inputs, so a page copying this would place the piece without it.

<caos-mention-picker id="mentionPicker1" aria-label="Mention suggestions"></caos-mention-picker>
// What an attribute cannot hold, set as a property — the same values the preview is drawn with.
const mentionPicker1 = document.getElementById('mentionPicker1');
mentionPicker1.candidates = [
{
"kind": "person",
"ref": "3f4c1b2a-5d6e-4f70-8a91-b2c3d4e5f607",
"label": "Morgan Hale",
"hint": "morgan.hale@example.test"
},
{
"kind": "record",
"ref": "account/6d5c4b3a-2918-4706-b5a4-938271605f4e",
"label": "Northgate Holdings",
"hint": "Account"
},
{
"kind": "record",
"ref": "project/0f1e2d3c-4b5a-4968-8776-655443322110",
"label": "Northgate retrofit",
"hint": "Project"
}
];

Said in the surface’s own words rather than left as a blank list.

Cannot be shown as a page placement: is given aria-label, which is set on the element rather than declared as an input. A stored placement carries only a component’s own inputs, so a page copying this would place the piece without it.

<caos-mention-picker id="mentionPicker1" aria-label="Mention suggestions" empty-label="Nobody here by that name."></caos-mention-picker>
// What an attribute cannot hold, set as a property — the same values the preview is drawn with.
const mentionPicker1 = document.getElementById('mentionPicker1');
mentionPicker1.candidates = [];