# Custom rendering

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

Canonical: https://reactmention.com/docs/recipes/custom-rendering



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 [#rich-item-content]

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

```tsx
<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 [#grouping-and-dividers]

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

```tsx
<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()` [#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:

```tsx
"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 [#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:

```tsx
{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 [#imperative-control]

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

```tsx
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.
