Directive

Mention

@-style mention picker for <textarea> and <input>. Detects trigger characters at word boundaries, opens an overlay at the caret, commits the selection back into the field.

Installation

import { WrMention, type WrMentionItem } from 'ngwr/mention';

@Component({ imports: [WrMention, FormsModule] })
export class MyComponent {
  protected readonly users = [{ label: 'Ada' }, { label: 'Alan' }];
}

Basic

Type @ at the start or after a space to open the picker. Arrows navigate, Enter / Tab commits, Esc cancels.

<textarea
  wrMention
  [wrMentionItems]="users"
  [(ngModel)]="text"
></textarea>

<!-- Type @ to open the picker. Arrows + Enter / Tab to commit, Esc to cancel. -->

Custom trigger

triggers accepts any character (or set of characters). Use # for tags, : for emoji, etc.

<textarea
  wrMention
  [wrMentionItems]="tags"
  [triggers]="['#']"
  [(ngModel)]="text"
></textarea>

Types

Data shapes used by the inputs and outputs above.

interface WrMentionItem {
  label: string;
  [key: string]: unknown;
}
NameDescriptionTypeDefault
WrMentionItemA mentionable entry — extend with any extra metadata.interface
labelrequiredText inserted and shown in the panel.string
[key: string]Anything else your app needs back on (selected).unknown

Accessibility

The field keeps its native role=\textbox\` — it holds prose, and the mention is a fragment inside it, not the field's value. It is deliberately not a combobox: that role is disallowed on <textarea> and would drop aria-multiline`, so a screen reader would stop reporting the field as multi-line for the whole editing session. Nothing here is configurable, and nothing is required of you.

<!-- What the directive renders. You write none of this. -->
<textarea
  wrMention
  aria-autocomplete="list"      <!-- static: a permanent capability of the field, -->
  aria-haspopup="listbox"       <!-- announced on focus before you type a trigger -->
  aria-controls="wr-mention-listbox-1"
  aria-activedescendant="wr-mention-listbox-1-opt-2"   <!-- only while open -->
></textarea>

<!-- In the overlay: -->
<ul id="wr-mention-listbox-1" role="listbox" aria-label="Mentions">
  <li id="wr-mention-listbox-1-opt-0" role="option" aria-selected="false">Ada Lovelace</li>
  <li id="wr-mention-listbox-1-opt-2" role="option" aria-selected="true">Grace Hopper</li>
</ul>

<!-- Plus a polite live region on <body>, because aria-expanded is not a
     supported state of role=textbox — so "a list appeared" and "X was
     inserted" have nowhere else to go:  "Matches available: 4" -->

API

NameDescriptionTypeDefault
wrMentionItemsItems to filter against the typed query.readonly T[][]
triggersTrigger characters that open the panel.readonly string[]['@']
displayWithMaps an item to its display label.(item: T) => string`item.label`
valueWithReturns the text to insert into the textarea on commit. Receives the picked item and the trigger char.((item: T, trigger: string) => string) | null`${trigger}${displayWith(item)}`
filterWithCustom filter.((query: string, item: T) => boolean) | nullcase-insensitive `includes` over `displayWith(item)`.
maxResultsMaximum number of items shown in the panel.number8
(wrMentionSelected)Emits the selected item, trigger, and query whenever the user commits.WrMentionCommit<T>