Recipes

Multiple triggers

Give each trigger its own items and insertion format.

Markdown Source

Use a triggers map to share one editor between channels.

"use client";

import { Mention, type UseMentionMultiProps } from "@danielivanov/mention";

type Person = { id: string; name: string };
type Channel = { slug: string };
type Channels = { "@": Person; "#": Channel };

const triggers: UseMentionMultiProps<Channels>["triggers"] = {
  "@": {
    items: [{ id: "alice", name: "Alice" }],
    getKey: p => p.id,
    getLabel: p => p.name,
  },
  "#": {
    items: [{ slug: "general" }],
    getKey: c => c.slug,
    getLabel: c => c.slug,
  },
};

export function Composer() {
  return (
    <Mention.Root<Channels> triggers={triggers}>
      <Mention.Input aria-label="Message" />
      <Mention.Popover aria-label="Suggestions">
        <Mention.List<Person> trigger="@">
          {p => <Mention.Item value={p}>{p.name}</Mention.Item>}
        </Mention.List>
        <Mention.List<Channel> trigger="#">
          {c => <Mention.Item value={c}>{c.slug}</Mention.Item>}
        </Mention.List>
        <Mention.Empty>Nothing found</Mention.Empty>
      </Mention.Popover>
    </Mention.Root>
  );
}

Each list's generic must match its channel. React context cannot check that relationship across separate Root and List elements. If you need the optional onSelect notification, its payload preserves the declared channel types: check "@" in payload to read a person from payload["@"], or read a channel from payload["#"] in the other branch.

The closest valid trigger before the caret determines the active channel. Only that channel is queried. Switching channels aborts the previous request and immediately hides its results. Arrays are filtered locally; fetchers receive the current query and an abort signal.

Each trigger must be one non-whitespace UTF-16 character. The same boundary rules apply to all triggers: start of input, whitespace, or a supported Unicode soft boundary. Multi-character sequences and emoji triggers are not supported.

useMentionMulti<Channels>() exposes the same behavior for custom layouts. Its items is a union of channel item types; activeTrigger identifies the active channel. It is not a TypeScript discriminated union between those two separate properties.

Inline channel objects and label functions are supported. Keep async fetcher references stable when their behavior has not changed, since a new fetcher starts a new request.