Components

Caret-Anchored Mention Popover

Type @ and the people popover opens at the text caret, not under the field. Textareas won't give you the caret's x·y, so a hidden mirror re-typesets everything before it and reads a marker's offset. The popover glides after the caret as you type, its rows cascade in, its height eases as the list filters, and it clamps to the card and flips above when the screen runs out. ↑↓ + Enter pick a person while focus never leaves the textarea (aria-activedescendant), and inserted names render as pills painted by a transparent overlay behind the real text. Tick Anchor to field to feel the usual shortcut.

@ to mention

Popover: closed · Last mention: none

Install

npx moumenlab add caret-mention-popover

For AI

Open .md

Usage

"use client";

import MentionComposer from "./caret-mention-popover";

export default function MentionComposerExample() {
  return (
    <div className="flex w-full max-w-sm flex-col gap-8">
      {/* Type @ to mention. Pass your people and handle onSubmit(text, mentions). */}
      <MentionComposer
        people={[
          { name: "Sarah Chen", handle: "sarahchen" },
          { name: "Omar Farouk", handle: "omarfarouk" },
          { name: "June Park", handle: "junepark" },
        ]}
        onSubmit={(text, mentions) => console.log("send", text, mentions)}
      />

      {/* anchor="field" is the usual dropdown shortcut - under the field, not the caret. */}
      <MentionComposer anchor="field" />
    </div>
  );
}

Story

  1. An experiment about one character

    No origin story here, just a question: how do you know an “@” means a mention? Only at a boundary, it turns out. The one in me@example.com must open nothing, so the trigger is the @ that follows the start of the text, a space, or an opening bracket, and it has to be the token the caret is sitting in right now.

  2. The other half is the box's width

    A textarea won't say where its caret is: selectionStart is a character index, no x or y. So an invisible mirror takes the field's exact width and typography, wraps the text the same way, and reports the caret's offset. Width decides the rest too: the popover clamps to the box and flips above when the viewport runs out, while its origin stays pointed at the @.

  3. The list is the part still to build

    Being honest about where this stops: the popover offers the whole roster. It should offer three or four people, and only the ones already in the conversation this box belongs to. Relevance is the enhancement, and it is not in here yet.

References

  • selectionStart (MDN): the whole constraint in one property: a character index and nothing else, which is why the caret's position has to be measured rather than read.