A React component that enables Facebook/Twitter-style @mentions and tagging in textarea inputs with full TypeScript support.
- ✅ Flexible Triggers - Use any character or pattern to trigger mentions (@, #, :, or custom)
- 🎨 Tailwind v4 Ready - First-class support for Tailwind CSS v4 utility styling
- ⚡ Async Data Loading - Load suggestions dynamically from APIs
- 🔍 Smart Suggestions - Real-time filtering and matching
- 🪄 Caret Aware - Detect when the caret overlaps mentions and style them via data attributes
- ♿ Accessible - Built with ARIA labels and keyboard navigation
- 🎯 TypeScript First - Written in TypeScript with complete type definitions
- 🧪 Well Tested - Comprehensive test suite with Testing Library
- 🌐 SSR Compatible - Works with Next.js and other SSR frameworks
- 📱 Mobile Friendly - Touch-optimized for mobile devices
# npm
npm install react-mentions-ts --save
# yarn
yarn add react-mentions-ts
# pnpm
pnpm add react-mentions-tsReact Mentions TS uses peer dependencies for its styling helpers and React runtime. Ensure these are installed in your application (skip any you already have):
# npm
npm install class-variance-authority clsx react react-dom tailwind-merge
# yarn
yarn add class-variance-authority clsx react react-dom tailwind-merge
# pnpm
pnpm add class-variance-authority clsx react react-dom tailwind-mergeCheck package.json for the latest peer dependency version ranges.
import { useState } from 'react'
import { MentionsInput, Mention } from 'react-mentions-ts'
function MyComponent() {
const [value, setValue] = useState('')
return (
<MentionsInput value={value} onMentionsChange={({ value: nextValue }) => setValue(nextValue)}>
<Mention trigger="@" data={users} renderSuggestion={(entry) => <div>{entry.display}</div>} />
<Mention trigger="#" data={tags} />
</MentionsInput>
)
}@import "tailwindcss";
(...)
@import "react-mentions-ts/styles/tailwind.css";MentionsInput is the main component that renders the textarea control. It accepts one or multiple Mention components as children. Each Mention component represents a data source for a specific class of mentionable objects:
- 👥 Users -
@usernamementions - 🏷️ Tags -
#hashtagmentions - 📋 Templates -
{{variable}}mentions - 🎭 Emojis -
:emoji:mentions - ✨ Custom - Any pattern you need!
The MentionsInput component supports the following props:
| Prop name | Type | Default value | Description |
|---|---|---|---|
| value | string | '' |
The value containing markup for mentions |
| onMentionsChange | function ({ trigger, value, plainTextValue, idValue, mentionId, mentions, previousValue }) | undefined |
Called when the mention markup changes; receives the updated markup value, plain text, id-based text, the affected mention id (when applicable), active mentions, and the previous markup value |
| onMentionSelectionChange | function (selection, context) | undefined |
Called whenever the caret or selection overlaps one or more mentions; receives an ordered array of MentionSelection entries and a metadata context containing the current value, plain text, and mention identifiers |
| onKeyDown | function (event) | empty function | A callback that is invoked when the user presses a key in the mentions input |
| singleLine | boolean | false |
Renders a single line text input instead of a textarea, if set to true |
| autoResize | boolean | false |
When true, resizes the textarea to match its scroll height after each input change (ignored when singleLine is true) |
| anchorMode | 'caret' | 'left' |
'caret' |
Controls whether the overlay follows the caret ('caret') or pins to the control’s leading edge ('left') |
| onMentionBlur | function (event, clickedSuggestion) | undefined |
Receives an extra clickedSuggestion flag when focus left via the suggestions list |
| suggestionsPortalHost | DOM Element | undefined | Render suggestions into the DOM in the supplied host element. |
| inputRef | React ref | undefined | Accepts a React ref to forward to the underlying input element |
| suggestionsPlacement | 'auto' | 'above' | 'below' |
'below' |
Controls where the suggestion list renders relative to the caret ('auto' flips when space is limited) |
| a11ySuggestionsListLabel | string | '' |
This label would be exposed to screen readers when suggestion popup appears |
| customSuggestionsContainer | function(children) | empty function | Allows customizing the container of the suggestions |
| inputComponent | React component | undefined | Allows the use of a custom input component |
| suggestionsDisplay | 'overlay' | 'inline' |
'overlay' |
Choose between the traditional suggestions overlay and inline autocomplete hints |
| spellCheck | boolean | false |
Controls browser spell checking on the underlying input (disabled by default) |
| onSelect | function (event) | empty function | A callback that is invoked when the user selects a portion of the text in the input |
onMentionsChange receives an object with the following fields:
value: the latest markup string containing mentionsplainTextValue: the same content without mention markupidValue: the plain-text view with each mention display substituted for its identifier (useful for downstream parsing/search)mentionId: the identifier of the mention that triggered the change when thetrigger.typeis mention-specific (e.g.'mention-add'); otherwiseundefinedmentions: the mention occurrences extracted from the new valuepreviousValue: the markup string before the changetrigger: metadata about what caused the change.trigger.typeis one of'input','paste','cut','mention-add', or'mention-remove', and, when available,trigger.nativeEventreferences the originating DOM event (optional; do not rely on its exact shape). Regular text edits (typing, Backspace/Delete) usetrigger.type: 'input'.
onMentionSelectionChange receives an array of MentionSelection entries. The array is ordered by the mention positions in the current value and is empty when the caret/selection does not intersect with any mentions. Each entry includes:
- All fields from
MentionOccurrence(id,display,childIndex,index,plainTextIndex, and the resolveddataitem when available) plainTextStart/plainTextEnd: the inclusive/exclusive plain-text boundaries of the mentionserializerId: identifies whichMentionchild produced the selection (useful when multiple triggers share anid)selection: one of:'inside'– the caret is collapsed somewhere between the mention boundaries'boundary'– the caret is collapsed exactly on the start or end boundary'partial'– a range selection overlaps the mention without covering it completely'full'– the selection fully covers the mention
The callback fires on every selection change, so you can keep live state in sync with caret movement.
The optional context argument includes:
value/plainTextValue/idValue: the latest markup, display-based plain text, and id-based plain text representationsmentions: the mentions found in the current valuementionIds: the identifiers for the mentions covered by the current selection (ordered)mentionId: the identifier when the selection maps to a single mention; otherwiseundefined
Each data source is configured using a Mention component, which has the following props:
| Prop name | Type | Default value | Description |
|---|---|---|---|
| trigger | RegExp or string | '@' |
Defines the char sequence upon which to trigger querying the data source |
| data | array or function (search, callback) | null |
An array of the mentionable data entries (objects with id & display keys, or a filtering function that returns an array based on a query parameter |
| renderSuggestion | function (entry, search, highlightedDisplay, index, focused) | null |
Allows customizing how mention suggestions are rendered (optional) |
| markup | string | MentionSerializer |
'@[__display__](__id__)' |
Template string for stored markup, or pass a MentionSerializer instance for full control |
| displayTransform | function (id, display) | returns display |
Accepts a function for customizing the string that is displayed for a mention |
| onAdd | function ({id, display, startPos, endPos, serializerId}) | empty function | Callback invoked when a suggestion has been added (optional) |
| appendSpaceOnAdd | boolean | false |
Append a space when a suggestion has been added (optional) |
Need the legacy
markupcustomization? ImportcreateMarkupSerializerfromreact-mentionsand passmarkup={createMarkupSerializer(':__id__')}(or any other template) to keep markup/parse logic in sync without wiring a regex manually.
When passing a
RegExpastrigger, omit the global/gflag. The component clones the pattern internally; global regexes maintain sharedlastIndexstate and will skip matches across renders. Your customRegExpshould also be anchored to the end of the string with$to match only at the current cursor position, and it must contain two capturing groups: the first for the trigger and query (e.g.,@mention), and the second for just the query (e.g.,mention).
Want to allow spaces (or other advanced patterns) after a trigger? Pass a custom
RegExp—for exampletrigger={makeTriggerRegex('@', { allowSpaceInQuery: true })}—instead of relying on a boolean flag. ThemakeTriggerRegexutility handles the regex construction for you.
If a function is passed as the data prop, it receives the current search query and should return a promise that resolves with the list of suggestions.
type User = { id: string; display: string }
const fetchUsers = async (query: string): Promise<User[]> => {
const response = await fetch(`/api/users?search=${query}`)
return response.json()
}
<Mention trigger="@" data={fetchUsers} />React Mentions ships its markup with Tailwind utility classes. Consumers should have Tailwind configured in their application build so these classes compile to real CSS. If you do not use Tailwind you can still provide your own styles via className, CSS modules, or inline styles.
The components assume Tailwind is available in the consuming app. A minimal setup looks like:
// tailwind.config.js
module.exports = {
content: ['./src/**/*.{js,ts,jsx,tsx}'],
theme: { extend: {} },
plugins: [],
}/* src/index.css (or your global stylesheet) */
@import 'tailwindcss';
@import 'react-mentions-ts/styles/tailwind.css';The optional helper react-mentions-ts/styles/tailwind.css only declares an @source "../dist"; directive so Tailwind v4 can detect the library's utility classes inside node_modules/react-mentions-ts/dist. Including it keeps your Tailwind config clean and avoids adding explicit content globs for the package.
If you are still on Tailwind v3, add ./node_modules/react-mentions-ts/dist/**/*.{js,jsx,ts,tsx} to the content array instead of importing the helper file.
<MentionsInput style={customStyle}>
<Mention style={mentionStyle} />
</MentionsInput>Every rendered mention (both in the hidden highlighter and the user-editable input) now exposes a data-mention-selection attribute whenever the caret or selection overlaps it. The attribute reflects the current coverage (inside, boundary, partial, or full), so you can target focus states without extra bookkeeping:
<Mention
trigger="@"
data={users}
className="rounded-full bg-indigo-500/25 px-2 py-0.5 text-sm font-semibold text-indigo-100 transition
data-[mention-selection=inside]:bg-emerald-500/35 data-[mention-selection=inside]:text-emerald-50
data-[mention-selection=boundary]:ring-2 data-[mention-selection=boundary]:ring-indigo-300
data-[mention-selection=partial]:bg-amber-500/35 data-[mention-selection=partial]:text-amber-50
data-[mention-selection=full]:bg-indigo-500 data-[mention-selection=full]:text-white"
/>See the “Caret mention states” demo (demo/src/examples/MentionSelection.tsx) for a complete example that combines styling with the onMentionSelectionChange callback.
When suggestionsDisplay="inline", override the inlineSuggestion style slot to customize the inline hint (the default demo style lives in demo/src/examples/defaultStyle.ts).
See demo/src/examples/defaultStyle.ts for examples.
Simply assign a className prop to MentionsInput. All DOM nodes will receive derived class names:
<MentionsInput className="mentions">
<Mention className="mentions__mention" />
</MentionsInput>Due to React Mentions' internal cursor tracking, use @testing-library/user-event for realistic event simulation:
import { render } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
test('mentions work correctly', async () => {
const user = userEvent.setup()
const { getByRole } = render(<MyMentionsComponent />)
await user.type(getByRole('textbox'), '@john')
// assertions...
})This project is a TypeScript rewrite and modernization of the original react-mentions library.
Made with contrib.rocks.