Custom rendering
Customize option content or use the hook for application-owned layout and positioning.
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.