Mentions

Capture @-mentions in a textarea with a markup value and suggestions dropdown

Usage

Mentions is a textarea that opens a suggestions dropdown when the user types a trigger character, for example @ or #. Picking a suggestion inserts a mention into the value. Mentions are atomic: deleting any part of a mention removes the whole mention.

import { useState } from 'react';
import { Mentions } from '@mantine/core';

const users = [
  { id: 'walter', display: 'Walter White' },
  { id: 'jesse', display: 'Jesse Pinkman' },
  { id: 'saul', display: 'Saul Goodman' },
  { id: 'gus', display: 'Gustavo Fring' },
];

function Demo() {
  const [value, setValue] = useState('Hey @[Walter White](walter), are you ready?');

  return (
    <Mentions
      label="Comment"
      placeholder="Type @ to mention someone"
      value={value}
      onChange={(payload) => setValue(payload.value)}
      minRows={3}
    >
      <Mentions.Mention trigger="@" data={users} />
    </Mentions>
  );
}

Value and markup

Mentions value is a markup string: each mention is stored as @[__display__](__id__), while the user sees only the display text. For example, the value:

'Hey @[Walter White](walter), are you there?';

is displayed as Hey Walter White, are you there?. When name is set, the markup value is submitted with native forms.

By default, the trigger character is not displayed. Set withTrigger on Mentions.Mention to display the same value as Hey @Walter White, are you there?:

import { Mentions } from '@mantine/core';

const users = [
  { id: 'walter', display: 'Walter White' },
  { id: 'jesse', display: 'Jesse Pinkman' },
  { id: 'saul', display: 'Saul Goodman' },
];

function Demo() {
  return (
    <Mentions
      label="The @ prefix is kept in the displayed text"
      defaultValue="Ping @[Walter White](walter) about it"
      minRows={3}
    >
      <Mentions.Mention trigger="@" data={users} withTrigger />
    </Mentions>
  );
}

Custom mention rendering

Use renderMention prop to customize how mentions are displayed, for example to add a tooltip or a HoverCard. The returned content must take the same space as the mention text, otherwise mentions are misaligned with the input text:

import { Avatar, Group, HoverCard, Mentions, Stack, Text } from '@mantine/core';

const usersData: Record<string, { display: string; role: string; email: string }> = {
  walter: { display: 'Walter White', role: 'Chemistry teacher', email: 'walter@example.com' },
  jesse: { display: 'Jesse Pinkman', role: 'Associate', email: 'jesse@example.com' },
  saul: { display: 'Saul Goodman', role: 'Attorney', email: 'saul@example.com' },
};

const users = Object.entries(usersData).map(([id, user]) => ({ id, display: user.display }));

function Demo() {
  return (
    <Mentions
      label="Hover a mention to see user details"
      defaultValue="Ping @[Walter White](walter) and @[Jesse Pinkman](jesse)"
      minRows={3}
      renderMention={({ id, display }) => {
        const user = usersData[id];
        return (
          <HoverCard withArrow shadow="md" openDelay={200} position="top" width={220}>
            <HoverCard.Target>
              <span>{display}</span>
            </HoverCard.Target>
            <HoverCard.Dropdown>
              <Group gap="sm" wrap="nowrap">
                <Avatar radius="xl" name={user?.display} color="initials" />
                <Stack gap={0}>
                  <Text fw={500}>{user?.display}</Text>
                  <Text size="xs" c="dimmed">{user?.role}</Text>
                  <Text size="xs">{user?.email}</Text>
                </Stack>
              </Group>
            </HoverCard.Dropdown>
          </HoverCard>
        );
      }}
    >
      <Mentions.Mention trigger="@" data={users} />
    </Mentions>
  );
}

Custom suggestions

Use renderSuggestion on Mentions.Mention to customize suggestions in the dropdown. item is the data item with all of its additional fields:

import { Avatar, Group, Mentions, Text } from '@mantine/core';

const users = [
  { id: 'walter', display: 'Walter White', email: 'walter@example.com' },
  { id: 'jesse', display: 'Jesse Pinkman', email: 'jesse@example.com' },
  { id: 'saul', display: 'Saul Goodman', email: 'saul@example.com' },
  { id: 'gus', display: 'Gustavo Fring', email: 'gus@example.com' },
];

function Demo() {
  return (
    <Mentions label="Custom suggestions" placeholder="Type @ to mention someone" minRows={3}>
      <Mentions.Mention
        trigger="@"
        data={users}
        renderSuggestion={({ item }) => (
          <Group gap="sm" wrap="nowrap">
            <Avatar size="sm" radius="xl" name={item.display} color="initials" />
            <div>
              <Text size="sm">{item.display}</Text>
              <Text size="xs" c="dimmed">
                {item.email}
              </Text>
            </div>
          </Group>
        )}
      />
    </Mentions>
  );
}

onChange payload

Unlike other Mantine inputs, Mentions onChange is called with an object instead of a string:

import { Mentions } from '@mantine/core';

function Demo() {
  return (
    <Mentions
      onChange={(payload) => {
        payload.value; // Markup value: "Hey @[Walter White](walter)"
        payload.plainTextValue; // What the user sees: "Hey Walter White"
        payload.idValue; // Display replaced by id: "Hey walter"
        payload.mentions; // Array of mentions present in the value
        payload.mentionId; // Id of the mention that triggered the change, if any
        payload.previousValue; // Previous markup value
        payload.reason; // { type, nativeEvent } – what caused the change
      }}
    >
      <Mentions.Mention trigger="@" data={[{ id: 'walter', display: 'Walter White' }]} />
    </Mentions>
  );
}

payload.reason.type is one of:

  • 'input' – regular typing or deletion that does not remove a mention
  • 'mention-add' – a suggestion was inserted (payload.mentionId is set)
  • 'mention-remove' – an edit removed one or more mentions
  • 'paste' – text was pasted
  • 'cut' – a selection was cut
  • 'insert-text' – text was dragged and dropped into the field
  • 'undo' and 'redo' – the change came from the undo history

Usage with form

Mentions onChange receives a payload object, so form.getInputProps alone does not store the markup string. Spread getInputProps for error and onBlur, then set value and onChange yourself:

import { Mentions } from '@mantine/core';
import { useForm } from '@mantine/form';

function Demo() {
  const form = useForm({ initialValues: { comment: '' } });

  return (
    <Mentions
      {...form.getInputProps('comment')}
      value={form.values.comment}
      onChange={(payload) => form.setFieldValue('comment', payload.value)}
    >
      <Mentions.Mention trigger="@" data={users} />
    </Mentions>
  );
}

onAdd

onAdd on Mentions.Mention is called when a suggestion is inserted. startPos and endPos are positions in the text the user sees, not in the markup value:

import { Mentions } from '@mantine/core';

function Demo() {
  return (
    <Mentions>
      <Mentions.Mention
        trigger="@"
        data={[{ id: 'walter', display: 'Walter White' }]}
        onAdd={({ id, display, startPos, endPos }) => {
          console.log(`Mentioned ${display} (${id}) at ${startPos}–${endPos}`);
        }}
      />
    </Mentions>
  );
}

Copy and paste

Mentions are kept when text is copied or cut and then pasted into a Mentions field that has the same triggers. Other applications receive plain text. A mention that is selected only partially is copied as plain text.

Multiple triggers

Add one Mentions.Mention for each trigger. The markup starts with the trigger: mentions of the # source below are stored as #[__display__](__id__):

import { Mentions } from '@mantine/core';

const users = [
  { id: 'walter', display: 'Walter White' },
  { id: 'jesse', display: 'Jesse Pinkman' },
  { id: 'saul', display: 'Saul Goodman' },
];

const tags = [
  { id: '1', display: 'announcement' },
  { id: '2', display: 'question' },
  { id: '3', display: 'bug' },
];

function Demo() {
  return (
    <Mentions label="Mention people with @ and tag topics with #" minRows={3}>
      <Mentions.Mention trigger="@" data={users} />
      <Mentions.Mention trigger="#" data={tags} appendSpaceOnAdd />
    </Mentions>
  );
}

RegExp triggers

trigger also accepts a RegExp that matches the trigger character. Mentions of a RegExp trigger are stored as @[__display__](__id__), use markup prop to change that. For example, the source below opens the dropdown on both @ and +:

import { Mentions } from '@mantine/core';

function Demo() {
  return (
    <Mentions>
      <Mentions.Mention trigger={/[@+]/} data={users} />
    </Mentions>
  );
}

To control how the query is matched, use a RegExp with two capture groups. It must be anchored to the end of the text with $, group 1 must capture the trigger with the query and group 2 the query.

Data sources

data prop of Mentions.Mention accepts an array of { id, display } items or a function that returns matches. Array data is filtered by the query: an item matches when its display or id contains the query, case-insensitively.

  • Set ignoreAccents to ignore accents when filtering array data, for example to match José with jose
  • Set allowSpaceInQuery to allow spaces in the query. By default, the query ends at the first whitespace.

Custom filter

Use filter prop of Mentions to change how array data is filtered. Set filter={false} to display array data as is, for example when it is already filtered on the server.

import { Mentions, MentionsFilter } from '@mantine/core';

const startsWithFilter: MentionsFilter = ({ query, items }) =>
  items.filter((item) => (item.display ?? item.id).toLowerCase().startsWith(query.toLowerCase()));

function Demo() {
  return (
    <Mentions filter={startsWithFilter}>
      <Mentions.Mention trigger="@" data={[{ id: 'walter', display: 'Walter White' }]} />
    </Mentions>
  );
}

maxSuggestions

Set maxSuggestions on Mentions.Mention to limit the number of displayed suggestions:

import { Mentions } from '@mantine/core';

function Demo() {
  return (
    <Mentions>
      <Mentions.Mention trigger="@" data={users} maxSuggestions={5} />
    </Mentions>
  );
}

Async data

data function can be asynchronous. It is called with the query and an object with signal that is aborted when the request becomes stale. Use debounceMs to debounce requests:

import { Mentions } from '@mantine/core';

const database = [
  { id: 'walter', display: 'Walter White' },
  { id: 'jesse', display: 'Jesse Pinkman' },
  { id: 'saul', display: 'Saul Goodman' },
  { id: 'gus', display: 'Gustavo Fring' },
  { id: 'mike', display: 'Mike Ehrmantraut' },
];

async function searchUsers(query: string, { signal }: { signal: AbortSignal }) {
  await new Promise((resolve) => setTimeout(resolve, 500));
  signal.throwIfAborted();
  return database.filter((user) => user.display.toLowerCase().includes(query.toLowerCase()));
}

function Demo() {
  return (
    <Mentions
      label="Async data source"
      placeholder="Type @ to search users"
      nothingFoundMessage="No users found"
      minRows={3}
    >
      <Mentions.Mention trigger="@" data={searchUsers} debounceMs={200} />
    </Mentions>
  );
}

Loading, empty and error states

Use loadingMessage, errorMessage and nothingFoundMessage props of Mentions to change messages displayed in the dropdown. To change empty and error states of a single source, use renderEmpty and renderError on Mentions.Mention:

import { Mentions } from '@mantine/core';

function Demo() {
  return (
    <Mentions
      loadingMessage="Searching users…"
      errorMessage="Could not load users"
      nothingFoundMessage="No users found"
    >
      <Mentions.Mention
        trigger="@"
        data={searchUsers}
        renderEmpty={(query) => `No users match "${query}"`}
        renderError={(query) => `Search for "${query}" failed`}
      />
    </Mentions>
  );
}

Custom markup

Use markup prop of Mentions.Mention to change how mentions are stored, for example {{__id__}}. If the format cannot be described with a template, pass a serializer object with insert and findAll methods.

With the default markup, id must not contain ) and display must not contain ](. If your data can contain these characters, use a serializer object.

displayTransform

displayTransform on Mentions.Mention changes the displayed text of mentions without changing the value:

import { Mentions } from '@mantine/core';

function Demo() {
  return (
    <Mentions defaultValue="Assigned to @[Walter White](walter)">
      <Mentions.Mention
        trigger="@"
        data={users}
        displayTransform={(id, display) => display.split(' ')[0]}
      />
    </Mentions>
  );
}

The value above is displayed as Assigned to Walter, while the markup still stores the full Walter White display.

Mention styles

Set className and style on Mentions.Mention to style mentions of a single source:

import { Mentions } from '@mantine/core';

function Demo() {
  return (
    <Mentions>
      <Mentions.Mention trigger="@" data={users} />
      <Mentions.Mention
        trigger="#"
        data={tags}
        style={{ backgroundColor: 'var(--mantine-color-teal-light)' }}
      />
    </Mentions>
  );
}

To style mentions of all sources, use the highlight Styles API selector or the --mentions-highlight-color and --mentions-highlight-bg CSS variables.

Anchor mode

By default, the dropdown is displayed below the caret. Set anchorMode="left" to align it with the left edge of the input. Use suggestionsPlacement to display it above the input (above) or on the side with more space (auto).

Inline suggestions

Set suggestionsDisplay="inline" to display the first match as ghost text at the caret instead of the dropdown. The ghost text is displayed only when the match starts with the query. Press Tab or Enter to accept it:

Type @ then press Tab, Enter or → to accept the suggestion

import { Mentions } from '@mantine/core';

const users = [
  { id: 'walter', display: 'Walter White' },
  { id: 'jesse', display: 'Jesse Pinkman' },
  { id: 'saul', display: 'Saul Goodman' },
];

function Demo() {
  return (
    <Mentions
      label="Inline suggestions"
      description="Type @ then press Tab, Enter or → to accept the suggestion"
      suggestionsDisplay="inline"
      minRows={3}
    >
      <Mentions.Mention trigger="@" data={users} appendSpaceOnAdd />
    </Mentions>
  );
}

Caret-aware styling

Mentions have data-mention-selection attribute that you can use to style them based on the caret position and selection:

  • inside – the caret is inside the mention
  • boundary – the caret is at the edge of the mention
  • partial – a part of the mention is selected
  • full – the whole mention is selected
import { Mentions } from '@mantine/core';
import classes from './Demo.module.css';

const users = [
  { id: 'walter', display: 'Walter White' },
  { id: 'jesse', display: 'Jesse Pinkman' },
];

function Demo() {
  return (
    <Mentions
      label="Move the caret over a mention to highlight it"
      defaultValue="Ping @[Walter White](walter) and @[Jesse Pinkman](jesse)"
      classNames={{ highlight: classes.highlight }}
      minRows={3}
    >
      <Mentions.Mention trigger="@" data={users} />
    </Mentions>
  );
}

onSelectionChange is called with the current selection and the mentions it overlaps.

Parsing mentions

Use getMentions function to get mentions from a markup value, for example on submit:

import { getMentions } from '@mantine/core';

const mentions = getMentions('Hey @[Walter White](walter)', [{ trigger: '@' }]);
// [{ id: 'walter', display: 'Walter White', index: 4, plainTextIndex: 4, trigger: '@' }]

@mantine/core also exports createMarkupSerializer to build a serializer from a template string and makeTriggerRegex to build a trigger RegExp.

Undo and redo

Mentions keeps its own undo history instead of the native one: undo and redo shortcuts restore removed mentions. Changes of a controlled value made outside of the component, for example a form reset, can be undone as well.

Single line

Set singleLine to render an input instead of a textarea:

import { Mentions } from '@mantine/core';

function Demo() {
  return (
    <Mentions singleLine label="Assignee" placeholder="Type @ to mention someone">
      <Mentions.Mention trigger="@" data={users} />
    </Mentions>
  );
}

Disabled and static

Set disabled to disable the input. Set mode="static" to display a value with mentions that cannot be edited:

import { Mentions } from '@mantine/core';

function Demo() {
  return (
    <Mentions mode="static" defaultValue="Ping @[Walter White](walter) about it">
      <Mentions.Mention trigger="@" />
    </Mentions>
  );
}

Accessibility

Set label or aria-label prop to label the input. The suggestions list is labelled Suggestions, use a11ySuggestionsListLabel prop to change or translate the label.

Keyboard interactions while the suggestions dropdown is open:

  • ArrowDown / ArrowUp – highlight the next/previous suggestion
  • Enter / Tab – insert the highlighted suggestion
  • Escape – close the dropdown. Inside Modal or Drawer, the first Escape closes the dropdown only

Keyboard interactions with suggestionsDisplay="inline" while the ghost text is visible:

  • Tab / Enter – accept the suggestion
  • → – accept the suggestion when the caret is at the end of the text
  • Escape – dismiss the suggestion

Styles API

Styles API

Mentions supports the Styles API; you can add styles to any inner element of the component with the classNames prop. Follow the Styles API documentation to learn more.

Description

Component Styles API

Hover over selectors to highlight corresponding elements

/*
 * Hover over selectors to apply outline styles
 *
 */