Troubleshooting
Common pitfalls and how to debug mention behaviour fast.
Popover doesn't open
Most "doesn't open" reports are one of three causes:
- Trigger sits mid-word. The library suppresses mid-word triggers —
foo@baris not a mention, it's an email. To trigger, the character before@must be whitespace, start-of-input, or a CJK / Thai / Khmer / Lao / Myanmar character. Type a space first, or override the trigger character. - Composition in progress. During IME composition (Japanese, Chinese Pinyin), the library suppresses dispatch until
compositionend. Commit the composition (Enter on the IME) and the popover opens on the post-commit value. - Items returned an empty array on initial query. With a sync
items=[], the popover opens but<Mention.Empty>renders. With an async fetcher that returns[]immediately, the same happens — verify withuseMention()'sstatusfield; if it's"success"anditems.length === 0, that's the empty branch.
Popover appears far from the caret
The caret-anchored popover uses a mirror-div technique that copies a long list of style properties from the textarea to compute pixel-correct caret coordinates. Mismatches cause drift:
- Custom fonts loading after mount. When the font swaps in, the textarea's character widths change but the popover position was computed against the fallback font's metrics. The library updates on
querychange, so the next keystroke re-anchors. For a one-shot fix at the moment of font load, force a re-anchor by toggling input focus. - Transformed ancestors. If a parent has
transform: scale(0.95)(or similar), the textarea'sgetBoundingClientRect()reflects the transformed bounds while the mirror runs at native scale. Avoid transforms on ancestors of the textarea, or compose your own anchor viauseMention(). - CSS
zoom. Same class of problem astransform: scale. Avoid on ancestors.
key warning in React 19 dev
React 19 strict-mode warns when key arrives via spread. Don't pass key through getItemProps:
// ❌ wrong — key arrives via spread
{items.map((u, i) => (
<li {...m.getItemProps(u, i)}>…</li>
))}
// ✅ right — key is explicit
{items.map((u, i) => (
<li key={getKey(u)} {...m.getItemProps(u, i)}>…</li>
))}The compound <Mention.List> handles this internally — only escape-hatch users see this.
aria-allowed-role axe violation
axe flags role="combobox" on <textarea> as not allowed. This is a documented exception — HTML-ARIA says no, ARIA 1.2 says yes, and the pattern ships in production at GitHub, Slack, Linear, and Ariakit. The runtime AT contract works (NVDA / JAWS / VoiceOver / TalkBack all narrate correctly); only the static linter complains. Suppress it via your axe config for the input element.
See Accessibility for the full list of documented exceptions.
Async fetcher: results flash and disappear
You forgot to forward the AbortSignal. Without it, the in-flight request from the previous query resolves after the fresh one and overwrites the list. Always pass signal to every fetch inside the fetcher:
async function searchUsers(query: string, signal: AbortSignal) {
const r = await fetch(`/api/users?q=${query}`, { signal });
// ^^^^^^^^ ← this line
return r.json();
}Focus jumps when the popover opens
The library never moves focus — it stays on the textarea (the combobox-as-substring contract). If focus is jumping, either:
- A custom
onFocus/onBlurhandler is responding to popover open events. Check what fires when[aria-expanded="true"]toggles. <Mention.Popover container={someElement}>is portaling to a node with a focus trap (Radix Dialog, etc.). Passcontainer={null}to render in-place.
TalkBack swipe order skips the popover
Android's TalkBack walks the DOM in source order; portaled popovers render at document.body, which TalkBack reaches after the rest of the page. Pass <Mention.Popover container={null}> to render in-place — the popover is then a direct sibling of the textarea and TalkBack reaches it on the very next swipe.
Bundle size jumped after upgrading
The library's published surface is size-limit-enforced at 12 kB gzip. Upgrades that exceed it fail CI; if your bundle inflated past that, the cause is downstream (one of your dependencies) — check with npx source-map-explorer.
Still stuck?
Reproduce in a fresh sandbox using the quick-start snippet and open an issue with the repro link. Include the React version, a minimal Mention.Root config, and a video or screenshot of the broken state.