Quickstart

Add typed mention suggestions to a React textarea.

Markdown Source

Mention opens suggestions at the caret while someone writes. Start with a native textarea for plain text, or connect a rich-text editor when your document needs mention nodes, formatting, or editor history.

Install

Use an application with React 19, then install the package:

Install Mention
npm install @danielivanov/mention@^0.2.1

With Bun, use bun add @danielivanov/mention@^0.2.1. These examples require Mention 0.2.1. The stylesheet is optional; import it once for the default popup and option styles.

Try the interaction

Type @al below to find Alice. Use ArrowDown and ArrowUp to navigate; Enter or Tab inserts the highlighted result. Escape dismisses suggestions without changing the message. Focus stays in the textarea, and Enter without an active suggestion adds a new line.

Type @ to find a person. Use the arrow keys to choose, Enter to insert, and Escape to dismiss.

Selecting Alice inserts plain text, @Alice, followed by a separating space when needed. Arrays are filtered by a case-insensitive substring match on getLabel.

Add a composer

This is the complete source of the example above. The rendered example and this code block use the same file.

In a Next.js App Router application, keep "use client" at the top: state, event handlers, and item render functions belong in a client component. Style the native label, hint, and textarea with your application's existing styles. The optional package stylesheet styles the suggestions.

Composer.tsx
"use client";

import { Mention } from "@danielivanov/mention";
import { useId, useState } from "react";
import "@danielivanov/mention/styles.css";

type Person = { id: string; name: string };

const people: Person[] = [
  { id: "alice", name: "Alice" },
  { id: "bob", name: "Bob" },
  { id: "carol", name: "Carol" },
];

export function Composer() {
  const id = useId();
  const [message, setMessage] = useState("");

  return (
    <Mention.Root<Person>
      items={people}
      getKey={(person) => person.id}
      getLabel={(person) => person.name}
    >
      <label htmlFor={id}>Message</label>
      <p id={`${id}-hint`}>
        Type @ to find a person. Use the arrow keys to choose, Enter to insert,
        and Escape to dismiss.
      </p>
      <Mention.Input
        id={id}
        name="message"
        value={message}
        onChange={(event) => setMessage(event.currentTarget.value)}
        aria-describedby={`${id}-hint`}
        placeholder="Write a message…"
        rows={4}
      />
      <Mention.Popover container={null} aria-label="People">
        <Mention.List<Person>>
          {(person) => (
            <Mention.Item value={person}>{person.name}</Mention.Item>
          )}
        </Mention.List>
        <Mention.Empty>No people found. Try another name.</Mention.Empty>
      </Mention.Popover>
    </Mention.Root>
  );
}

getKey supplies each item's stable identity. getLabel supplies the local search label and default insertion label. Give Mention.List its item type explicitly: React context cannot infer that type from Mention.Root.

The input supports normal textarea props, including value, onChange, onBlur, onKeyDown, and ref. A consumer key handler runs first and can call preventDefault() to override Mention's handling. onSelect on Root is optional; use it only when you need a notification after insertion.

The example uses container={null} to keep the listbox within the article's landmark. Use a portal container when your application's clipping or layering requires one.

Choose your next step

Verify in your application

Check insertion, empty results, Escape, focus, and ordinary typing with your actual form state. Test the popup inside your dialogs and scrolling containers. For asynchronous data, also test requests resolving out of order.

Read Accessibility for the native textbox and listbox relationship, keyboard behavior, and manual verification guidance. Automated checks do not establish screen-reader or real OS IME compatibility.