A textarea-native mention & trigger primitive for React.
Headless, accessible, ~14 kB. @ mentions, # channels, / commands — any trigger you want, in one editor, with the WAI-ARIA combobox contract done right and no rich-text framework.
Same primitive, three contexts. The chrome changes; the contract doesn't.
Accessible by construction.
Combobox-as-substring done by the WAI-ARIA APG: textarea keeps focus, aria-activedescendant moves between virtual options, screen readers narrate changes without focus theft.
Multi-trigger by design.
One editor, any combination of triggers. Pass a triggers map keyed by the characters you choose — @ mentions, # channels, / commands, : emoji — each with its own item shape, render, and insert format. Channel switching, per-channel typing, and discriminated payloads all handled by the library; you write three render-props instead of three components.
Textarea-native.
Plain <textarea>. No contenteditable, no custom selection model, no rich-text framework. Forms submit it, password managers fill it, mobile keyboards behave.
Caret-anchored popover.
Mirror-div math measures the active line + column, then anchors via Floating UI's virtual element. The popover follows the caret pixel-perfectly across wraps and resizes.
i18n + IME safe.
CJK / Thai / Khmer / Lao / Myanmar word boundaries handled at the dispatcher; bidi-aware caret math for RTL; composition guards so Japanese, Pinyin, and Gboard never race past the trigger. The plumbing most mention libraries skip.
shadcn-friendly.
Theme tokens resolve through --popover, --accent, --border. Drop it in any shadcn project and it inherits. Or use the unstyled prop and drive every selector yourself.
Small.
~14 kB minified+gzipped, ceiling-enforced in CI. No editor framework, no DOM library, no Tailwind dependency, no portal hacks. The runtime is the reducer + a popover.
The bit that's interesting: the combobox-as-substring contract.
The textarea never gives up focus. The popover is a virtual listbox that the textarea points at — both via aria-controls and, per-keystroke, via aria-activedescendant. That contract is what lets the primitive ship without owning a single tab stop or selection model.
role="combobox" the entire time; screen readers announce option changes through aria-activedescendant as it retargets.