Recipes

Custom rendering

Customize option content or use the hook for application-owned layout and positioning.

Markdown Source

Most consumers can stay inside the compound API and pass children to <Mention.Item>. Use useMention() when your application needs to own popup layout and positioning. Its prop getters retain the listbox and option roles; a different interaction pattern needs its own accessibility design.

In Next.js App Router, place render functions and hook-based examples inside a component with a "use client" boundary.

Rich item content

<Mention.Item> accepts arbitrary children. Avatar + name + handle is the canonical layout:

<Mention.List<User>>
  {(u) => (
    <Mention.Item value={u}>
      <img src={u.avatar} alt="" className="size-6 rounded-full" />
      <span className="font-medium">{u.name}</span>
      <span className="text-muted-foreground">@{u.username}</span>
    </Mention.Item>
  )}
</Mention.List>

The library applies role="option", id, hover handlers, and aria-selected on the option element. You don't need to wire any of those.

Grouping and dividers

The render-prop is just a function — interleave any non-Mention.Item JSX freely:

<Mention.List<User>>
  {(u, i) => (
    <>
      {i === 0 || u.team !== items[i - 1].team ? (
        <div role="presentation" className="px-2 py-1 text-xs">
          {u.team}
        </div>
      ) : null}
      <Mention.Item value={u}>{u.name}</Mention.Item>
    </>
  )}
</Mention.List>

The heading is not an option. Its text can still be exposed to assistive technology; verify custom grouping and reading order in your application. Compare adjacent items from the current filtered results, not an unfiltered source array.

Escape hatch — useMention()

When <Mention.Popover>'s default container doesn't fit (you're building a sticky bottom bar, or wrapping in your own design-system Popover), pull the props directly:

"use client";

import { useMention } from "@danielivanov/mention";

type User = { id: string; name: string };
const users: User[] = [
  { id: "alice", name: "Alice" },
  { id: "bob", name: "Bob" },
];

export function CustomMention() {
  const m = useMention<User>({
    items: users,
    getKey: (u) => u.id,
    getLabel: (u) => u.name,
  });

  return (
    <div style={{ position: "relative" }}>
      <textarea {...m.getInputProps()} aria-label="Message" />
      {m.open ? (
        <div
          {...m.getPopoverProps()}
          aria-label="People"
          style={{ position: "absolute", top: "100%", left: 0 }}
        >
          {m.items.map((u, i) => (
            <div
              key={u.id} // ← required; getItemProps doesn't include `key`
              {...m.getItemProps(u, i)}
              style={{
                padding: "0.5rem",
                background: i === m.highlightedIndex ? "Highlight" : "Canvas",
                color: i === m.highlightedIndex ? "HighlightText" : "CanvasText",
              }}
            >
              {u.name}
            </div>
          ))}
          {m.status === "success" && m.items.length === 0 ? (
            <div>No people found</div>
          ) : null}
        </div>
      ) : null}
    </div>
  );
}

React 19 key warning

getItemProps deliberately does not include key — React 19's strict-mode dev warnings fire whenever key arrives via spread. Pass it explicitly:

{m.items.map((u, i) => (
  <div key={u.id} {...m.getItemProps(u, i)}>

  </div>
))}

The compound API (<Mention.List<User>>) handles this internally — only escape-hatch consumers need to remember it.

Imperative control

Pass handleRef for open() / close() / commit() / host access:

const handle = useRef<MentionImperativeHandle<User>>(null);

<Mention.Root handleRef={handle} /* … */>

</Mention.Root>

// later:
handle.current?.commit(suggestedUser);
handle.current?.host?.focus();

Opening rescans the current caret and requires an active trigger. Commit accepts only an item from the current results.