Mention
Accessible, unstyled trigger-character mentions for textareas, inputs, and contenteditable.
|
|
| Package |
@vizejs/ui/mention |
| Maturity |
stable |
| Own the source |
vize lib pull mention |
| Requires |
collection, composite-navigation, context, controllable-state, dismissable-layer, id, portal, positioner, presence |
| Aliases |
mention, mentions, at mention, trigger autocomplete, hashtag input |
| Covers |
WAI-ARIA combobox pattern (listbox popup), Ant Design Mentions, Mantine Mentions, Tiptap Suggestion |
Usage
import { Mention, MentionRoot, MentionContent, MentionEditable, MentionEmpty, MentionInput, MentionItem } from "@vizejs/ui/mention";
Or copy the source into your project with vize lib pull mention (see Source Distribution).
API
Mention<T>
Source: src/families/form/mention/mention-root.vue
Props
| Prop |
Type |
Default |
Description |
id |
string | null |
undefined |
Consumer-owned base id. null and undefined select a deterministic fallback. |
modelValue |
string |
undefined |
Controlled field text for MentionInput. undefined selects uncontrolled behavior. |
defaultValue |
string |
"" |
Initial field text for uncontrolled use. |
items |
readonly T[] |
undefined |
Candidate items filtered by the active query and exposed as filteredItems. |
loadItems |
MentionLoader<T> |
undefined |
Async item source; its results replace items. |
debounce |
number |
150 |
Debounce in milliseconds before loadItems runs for a changed query. |
triggers |
readonly MentionTrigger[] |
[{ char: "@" }] |
Trigger characters and their query rules. |
itemText |
(item: T) => string |
undefined |
Human-readable text for an item, used by filtering and the default insertion. |
filter |
MentionFilter<T> | false |
undefined |
Filter for items; false keeps every item (server-side filtering). |
insert |
MentionInsertTransform<T> |
undefined |
Text inserted for a chosen item. Defaults to trigger + item text + one space. |
open |
boolean |
undefined |
Controlled popup open state. The popup only shows while a trigger token is active. |
defaultOpen |
boolean |
false |
Initial open state for uncontrolled use. |
loop |
boolean |
false |
Wrap arrow-key navigation at the first and last item. |
disabled |
boolean |
false |
Disable trigger detection and editing. |
Events
| Event |
Payload |
Description |
update:modelValue |
[text: string] |
Fired when the field text requests a new controlled value. |
select |
[item: T, trigger: MentionTrigger] |
Fired after an item is inserted for the active token, with the item and its trigger. |
update:query |
[query: string] |
Fired when the active query changes; "" when no token is active. |
query-change |
[query: string | null, trigger: MentionTrigger | null] |
Fired when the active token changes, with its query and trigger or null for both when it ends. |
update:open |
[open: boolean] |
Fired when the popup requests a controlled open value. |
open-change |
[open: boolean, previous: boolean, nativeEvent: Event | null] |
Fired after any distinct open-state request with next state, previous state, and event. |
Slots
| Slot |
Slot props |
Description |
default |
MentionSlotState<T> |
The field (MentionInput or MentionEditable) and MentionContent. Receives query and item state. |
Exposed
| Member |
Type |
Description |
dismiss |
— |
|
focus |
— |
|
match |
— |
|
open |
— |
|
refresh |
— |
|
select |
— |
|
text |
— |
|
MentionRoot<T>
Source: src/families/form/mention/mention-root.vue
Props
| Prop |
Type |
Default |
Description |
id |
string | null |
undefined |
Consumer-owned base id. null and undefined select a deterministic fallback. |
modelValue |
string |
undefined |
Controlled field text for MentionInput. undefined selects uncontrolled behavior. |
defaultValue |
string |
"" |
Initial field text for uncontrolled use. |
items |
readonly T[] |
undefined |
Candidate items filtered by the active query and exposed as filteredItems. |
loadItems |
MentionLoader<T> |
undefined |
Async item source; its results replace items. |
debounce |
number |
150 |
Debounce in milliseconds before loadItems runs for a changed query. |
triggers |
readonly MentionTrigger[] |
[{ char: "@" }] |
Trigger characters and their query rules. |
itemText |
(item: T) => string |
undefined |
Human-readable text for an item, used by filtering and the default insertion. |
filter |
MentionFilter<T> | false |
undefined |
Filter for items; false keeps every item (server-side filtering). |
insert |
MentionInsertTransform<T> |
undefined |
Text inserted for a chosen item. Defaults to trigger + item text + one space. |
open |
boolean |
undefined |
Controlled popup open state. The popup only shows while a trigger token is active. |
defaultOpen |
boolean |
false |
Initial open state for uncontrolled use. |
loop |
boolean |
false |
Wrap arrow-key navigation at the first and last item. |
disabled |
boolean |
false |
Disable trigger detection and editing. |
Events
| Event |
Payload |
Description |
update:modelValue |
[text: string] |
Fired when the field text requests a new controlled value. |
select |
[item: T, trigger: MentionTrigger] |
Fired after an item is inserted for the active token, with the item and its trigger. |
update:query |
[query: string] |
Fired when the active query changes; "" when no token is active. |
query-change |
[query: string | null, trigger: MentionTrigger | null] |
Fired when the active token changes, with its query and trigger or null for both when it ends. |
update:open |
[open: boolean] |
Fired when the popup requests a controlled open value. |
open-change |
[open: boolean, previous: boolean, nativeEvent: Event | null] |
Fired after any distinct open-state request with next state, previous state, and event. |
Slots
| Slot |
Slot props |
Description |
default |
MentionSlotState<T> |
The field (MentionInput or MentionEditable) and MentionContent. Receives query and item state. |
Exposed
| Member |
Type |
Description |
text |
string |
Current field text. |
match |
MentionMatch | null |
Active token, or null. |
open |
boolean |
Whether the popup is open. |
select |
(item: T) => boolean |
Insert item for the active token and report whether text changed. |
dismiss |
() => void |
Close the popup until the caret enters a different token. |
refresh |
() => void |
Re-read text and caret from the field and re-detect the token. |
focus |
(options?: FocusOptions) => void |
Focus the field. |
MentionContent
Source: src/families/form/mention/mention-content.vue
Props
| Prop |
Type |
Default |
Description |
placement |
Placement |
"bottom-start" |
Preferred placement relative to the trigger character before collision handling. |
strategy |
PositionerStrategy |
"fixed" |
CSS positioning mode published on the floating host. |
offset |
number |
4 |
Gap between the caret line and the popup. |
collisionPadding |
number |
8 |
Viewport padding the popup should not cross. |
flip |
boolean |
true |
Flip to the opposite side when the preferred side overflows more. |
shift |
boolean |
true |
Shift the popup back into the viewport. |
size |
boolean |
true |
Publish available-size CSS variables and constrain the host. |
to |
string | HTMLElement |
"body" |
CSS selector or element the popup is moved into. |
portalDisabled |
boolean |
false |
Render in place instead of teleporting. |
defer |
boolean |
true |
Keep content in place until the portal target exists, avoiding SSR mismatch. |
forceMount |
boolean |
false |
Keep items mounted (hidden) while closed. |
ariaLabel |
string |
undefined |
Accessible name of the suggestion listbox. |
Events
| Event |
Payload |
Description |
escape-key-down |
[event: DismissableLayerEscapeKeyDownEvent] |
Fired before Escape closes the popup. Call preventDefault() to keep it open. |
pointer-down-outside |
[event: DismissableLayerPointerDownOutsideEvent] |
Fired before an outside pointer-down closes the popup. Call preventDefault() to keep it open. |
dismiss |
[event: DismissableLayerDismissEvent] |
Fired after an unprevented dismissal request closes the popup. |
Slots
| Slot |
Slot props |
Description |
default |
MentionContentSlotState |
MentionItems and MentionEmpty. Receives open state, placement, and the query. |
Exposed
| Member |
Type |
Description |
element |
Readonly<ShallowRef<HTMLElement | null>> |
Template ref to the rendered root element. |
MentionEditable
Source: src/families/form/mention/mention-editable.vue
Props
| Prop |
Type |
Default |
Description |
ariaLabel |
string |
undefined |
Accessible name when no visible label supplies one. |
ariaLabelledby |
string |
undefined |
Space-separated ids of visible labels. |
ariaDescribedby |
string |
undefined |
Space-separated ids that describe the field. |
multiline |
boolean |
true |
Announce the editor as multi-line. |
Slots
| Slot |
Slot props |
Description |
default |
— |
Initial editor content rendered on the server and adopted on hydration. |
Exposed
| Member |
Type |
Description |
element |
Readonly<ShallowRef<HTMLElement | null>> |
Template ref to the rendered root element. |
focus |
— |
|
MentionEmpty
Source: src/families/form/mention/mention-empty.vue
Slots
| Slot |
Slot props |
Description |
default |
{ readonly query: string } |
Message shown when no suggestion matches and nothing is loading. Receives the query. |
Source: src/families/form/mention/mention-input.vue
Props
| Prop |
Type |
Default |
Description |
as |
"input" | "textarea" |
"textarea" |
Native text control to render. |
name |
string |
undefined |
Form control name; the field submits its plain text natively. |
placeholder |
string |
undefined |
Hint text shown while empty. |
rows |
number |
undefined |
Visible text lines for the textarea. |
ariaLabel |
string |
undefined |
Accessible name when no visible label supplies one. |
ariaLabelledby |
string |
undefined |
Space-separated ids of visible labels. |
ariaDescribedby |
string |
undefined |
Space-separated ids that describe the field. |
Exposed
| Member |
Type |
Description |
element |
Readonly<ShallowRef<HTMLElement | null>> |
Template ref to the rendered root element. |
focus |
— |
|
MentionItem<T>
Source: src/families/form/mention/mention-item.vue
Props
| Prop |
Type |
Default |
Description |
id |
string | null |
undefined |
Consumer-owned option id. null and undefined select a deterministic fallback. |
value (required) |
T |
required |
Item inserted when this option is chosen. |
disabled |
boolean |
false |
Disable this option. |
textValue |
string |
undefined |
Explicit option text. undefined extracts the rendered text. |
Slots
| Slot |
Slot props |
Description |
default |
MentionItemSlotState<T> |
Option content. Receives the item, highlight, and disabled state. |
Exposed
| Member |
Type |
Description |
active |
— |
|
element |
Readonly<ShallowRef<HTMLElement | null>> |
Template ref to the rendered root element. |
select |
— |
|
Behavior
Contract
Mention is a headless trigger-character helper (@ people, # tags, :
emoji, …) for native text fields and contenteditable editors.
mention-core.ts is DOM-free: detectMention(text, caret, triggers) finds the
active token and applyMentionEdit replaces it with the text produced by an
insertion transform. mention-root.vue is generic over the item type T,
inferred from items or loadItems, and owns the text model, the active
token, filtering, the async loader, and an active-descendant collection built
on the collection registry and composite navigation.
mention-input.vue renders a native <textarea> (or <input role="combobox">
with as="input") and mention-editable.vue a role="textbox"
contenteditable. Both keep DOM focus and expose the highlighted option through
aria-activedescendant, aria-autocomplete="list", and aria-controls.
mention-content.vue renders the role="listbox" popup through Portal,
Presence, and a Positioner anchored to a virtual element at the trigger
character (a mirror element for text fields, a DOM Range for contenteditable),
with a dismissable layer for outside presses and Escape. mention-item.vue
renders role="option" and mention-empty.vue the no-results message. No CSS
is shipped.
Public Surface
| Surface |
Contract |
MentionRoot props |
id, modelValue, defaultValue, items, loadItems, debounce, triggers, itemText, filter, insert, open, defaultOpen, loop, disabled |
MentionRoot emits |
update:modelValue(text), select(item, trigger), update:query(query), query-change(query, trigger), update:open(open), open-change(open, previous, e) |
MentionRoot slot |
text, query, trigger, match, filteredItems, open, loading, status, error, state |
MentionRoot expose |
text, match, open, select, dismiss, refresh, focus |
MentionInput props |
as, name, placeholder, rows, ariaLabel, ariaLabelledby, ariaDescribedby |
MentionContent |
positioner props, forceMount, to, portalDisabled, defer, ariaLabel; emits escape-key-down, pointer-down-outside, dismiss |
| Trigger rules |
start of text or after whitespace/punctuation; @@ escapes; query ends at whitespace unless allowSpaces, always at a newline; pattern, minChars, maxChars |
Normative Behavior
| # |
State |
Input |
Outcome |
Proven by |
| M1 |
idle |
render |
textarea with aria-autocomplete="list", aria-haspopup="listbox", no aria-controls, no popup |
renders a textarea with listbox autocomplete semantics and no popup |
| M2 |
idle |
type a trigger + query |
opens, wires aria-controls, filters by query, highlights the first item, emits update:query/query-change; empty content shows |
typing a trigger opens the listbox, filters by query, and highlights the first item |
| M3 |
any |
mid-word trigger / caret leaves token |
never opens mid-word; moving the caret out of the token closes and emits query-change(null, null) |
mid-word triggers never open and moving the caret out of the token closes |
| M4 |
open |
ArrowDown/ArrowUp/Home/End/Enter/Tab/Escape |
arrows and Home/End move the highlight (loop wraps); Enter and Tab insert, move the caret, emit select; Escape dismisses until a new token |
keyboard matrix: arrows, Home/End, Enter inserts, Tab inserts, Escape dismisses |
| M5 |
closed / no match |
ArrowDown / Enter |
not consumed, so native editing and form submission proceed |
keys pass through while closed and Enter without a highlight is not consumed |
| M6 |
open |
pointer move / press / click |
highlights, keeps focus in the field, inserts and merges a doubled space |
clicking an item inserts it and pointer movement highlights |
| M7 |
several triggers |
# / @ tokens with insert |
each token uses its trigger; the transform receives item and trigger; as="input" renders role="combobox" with aria-expanded |
insertion transforms and multiple triggers receive the item and trigger |
| M8 |
contenteditable |
type / Enter |
detects the token from the DOM selection and replaces it in place, emitting the new text |
contenteditable fields detect tokens from the selection and insert text in place |
| M9 |
loadItems |
type / newer query / close |
loads per query and trigger, publishes loading, hides empty content meanwhile, aborts superseded requests, keeps loaded order |
loadItems loads per query, publishes loading, and aborts superseded requests |
| M10 |
controlled / API |
filter / dismiss / disabled |
injected filters replace the default; dismiss() closes; disabled fields never open |
controlled text, filter injection, disabled state, and exposed methods |
| M11 |
no provider |
mount a part |
throws VIZE_UI_CONTEXT_MISSING |
parts require a Mention provider |
| M12 |
caret helpers |
measure / locate / replace |
mirror measurement leaves no nodes behind; offsets map across text nodes; editable replacement edits in place |
caret helpers measure text fields and editable content without leaking mirrors |
| C1 |
core |
detect |
start and after punctuation; caret-based |
detects a trigger at the start and after whitespace or punctuation, uses the caret position, not the end of the text |
| C2 |
core |
reject |
mid-word, @@, whitespace, newline, out-of-range caret |
never triggers mid-word, after a repeated trigger, or across whitespace and newlines |
| C3 |
core |
trigger options |
pattern, minChars, allowSpaces, maxChars, multi-character triggers |
supports several triggers with patterns, minimum length, and spaces, multi-character triggers match as a unit |
| C4 |
core |
edit / compare / filter |
default insertion, caret after insertion, doubled-space merge, match equality, accent-insensitive filter |
applies edits with the default insertion and collapses a doubled space, compares matches and normalizes filter text |
SSR
No token is active on the server, so the popup renders closed and every id
comes from the deterministic-id primitive. Textarea and contenteditable trees
render byte-identical markup and hydrate without warnings (renders byte-identical Mention textarea markup across isolated requests, renders byte-identical contenteditable markup, hydrates textarea and contenteditable Mentions without mismatches). Caret measurement only runs in handlers and
positioner updates. For mention-editable.vue the editor DOM is the source of
truth: its text is read on input and written back on insertion.