Recipes

International input

Unicode trigger boundaries, right-to-left text, composition, and locale-aware search.

Markdown Source

Mention detects triggers in Unicode text and guards suggestion handling during composition. Test the result with the languages, fonts, browsers, and input methods your application supports.

Unicode trigger boundaries

A trigger opens suggestions at the start of the text, after whitespace, or after a character from selected scripts that do not always separate words with spaces: Han, Hiragana, Katakana, Hangul, Thai, Khmer, Lao, and Myanmar.

"こんにちは@田" // Opens at @, query = "田"
"한국어 @"      // Opens at @
"ภาษาไทย@"     // Opens at @
"foo@bar.com"   // Does not open: @ is inside a word

The detector checks the preceding character with Unicode property expressions. It does not use a locale-specific word segmenter.

Han characters followed by an email-like string

用户@example opens suggestions because the Han character before @ is a supported soft boundary. The detector cannot distinguish this from an intended mention at the same boundary. Escape dismisses the popup.

If this pattern conflicts with your application, choose another trigger, such as (U+FF20). Triggers must be one non-whitespace UTF-16 character; emoji and multi-character triggers are not supported.

Right-to-left text

Set dir="rtl" on the textarea or an ancestor:

<Mention.Input dir="rtl" aria-label="הודעה" />

The textarea mirror copies direction and text-layout styles to measure the caret. Mixed-direction content, fonts, wrapping, and the end of a line can affect browser geometry. Verify the popup with realistic Arabic or Hebrew text in your layout. See positioning troubleshooting if it drifts.

The application supplies labels, empty/loading messages, and insertion text. Use getInsertText when the default trigger plus label does not match your desired format.

IME composition

Mention.Input preserves the textarea's composition handling and rescans when composition ends. Mention leaves Enter used for IME confirmation to the host instead of committing a suggestion.

With a standalone hook, spread getInputProps() to preserve the same event handling. A rich editor adapter must return null from read() while composing and preserve its editor's composition behavior.

Synthetic composition tests check event handling; they do not verify real candidate-window behavior. The repository includes a manual smoke rig at packages/react/manual-at/ime/. Read Accessibility for the current verification boundary.

Name matching

By default, a space ends the query and synchronous arrays use a case-insensitive substring match on getLabel. Opt into allowSpaces to search full names such as @Alice Chen, and supply a synchronous filter to choose your application's matching rules:

const fold = (text: string) =>
  text.normalize("NFD").replace(/\p{M}/gu, "").toLowerCase();

type Person = { id: string; name: string };
const people: Person[] = [
  { id: "alice", name: "Alice Chen" },
  { id: "jose", name: "José García" },
];

<Mention.Root<Person>
  items={people}
  getKey={(person) => person.id}
  getLabel={(person) => person.name}
  allowSpaces
  filter={(person, query) => fold(person.name).includes(fold(query))}
>
  {/* Input, popover, and options */}
</Mention.Root>

Typing @jose gar now matches José García immediately. Accent folding is a policy for this example; it is not transliteration or a universal rule for all languages. Use Intl.Collator or another application-owned matcher when your language data needs different behavior. The filter runs during render, so keep it pure. No async wrapper or debounce is needed for local data.

Both options are per channel in a triggers map. Async fetchers receive the full query, including spaces when enabled, and their results are used as returned without applying filter.

Where a query ends

allowSpaces accepts Unicode horizontal space separators (Zs). Tabs, line breaks, and rich-editor atom placeholders (U+FFFC) always stop detection. The closest configured trigger wins.

After selecting a name or pressing Escape, continuing forward from that query keeps it dismissed. A new trigger or an earlier edit can reopen suggestions. Calling the handle's open() explicitly rescans even a dismissed query.

Textareas still contain ordinary text; this option does not track mention identity. Use an editor integration when you need structured mention nodes.