Vize

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.

MentionInput

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.