A React component that enables Facebook/Twitter-style @mentions and tagging in textarea inputs with full TypeScript support.
- Features
- Installation
- Quick Start
- How It Works
- Configuration
- More Examples
- Advanced Usage
- Package & Tree Shaking
- Styling
- Testing
- FAQ & Gotchas
- Migrating from react-mentions
- Contributing
- License
- Acknowledgments
- Flexible Triggers — any character, string, or custom
RegExp(@,#,:, or your own) - Async Data Loading — real-time filtering with debouncing,
AbortSignalcancellation, and cursor pagination - Caret Aware — detect when the caret overlaps mentions and style them via
data-mention-selection - Inline Autocomplete — ghost-text hints accepted with Tab, Enter, or arrow keys
- Tailwind v4 Ready — first-class support for Tailwind CSS v4 utility styling
- TypeScript First — written in TypeScript with complete type definitions
- Accessible — built with ARIA labels and keyboard navigation
- 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 and react-dom are required. The remaining peers are only needed if you use the built-in Tailwind styling:
# Required
npm install react react-dom
# Required only if using the built-in Tailwind utility classes
npm install class-variance-authority clsx tailwind-mergeCheck package.json for the latest peer dependency version ranges.
import { useState } from 'react'
import { MentionsInput, Mention } from 'react-mentions-ts'
const users = [
{ id: 'walter', display: 'Walter White' },
{ id: 'jesse', display: 'Jesse Pinkman' },
{ id: 'gus', display: 'Gustavo "Gus" Fring' },
{ id: 'saul', display: 'Saul Goodman' },
]
function MyComponent() {
const [value, setValue] = useState('')
return (
<MentionsInput value={value} onMentionsChange={({ value: nextValue }) => setValue(nextValue)}>
<Mention trigger="@" data={users} />
</MentionsInput>
)
}Most apps need both the text and the structured list of selected entities. Pass multiple Mention children for different triggers and read mentions from the change payload:
import { useState } from 'react'
import { MentionsInput, Mention } from 'react-mentions-ts'
const users = [
{ id: 'walter', display: 'Walter White' },
{ id: 'jesse', display: 'Jesse Pinkman' },
]
const tags = [
{ id: 'urgent', display: 'urgent' },
{ id: 'followup', display: 'follow-up' },
]
function CommentBox() {
const [value, setValue] = useState('')
const [mentionedIds, setMentionedIds] = useState<string[]>([])
return (
<>
<MentionsInput
value={value}
onMentionsChange={({ value: nextValue, mentions }) => {
setValue(nextValue)
setMentionedIds(mentions.map((m) => String(m.id)))
}}
>
<Mention trigger="@" data={users} appendSpaceOnAdd />
<Mention trigger="#" data={tags} markup="#[__display__](__id__)" appendSpaceOnAdd />
</MentionsInput>
<button type="button" onClick={() => submitComment({ body: value, mentionedIds })}>
Post
</button>
</>
)
}See Styling for the Tailwind setup (optional; the component also works with plain CSS, CSS modules, or inline styles).
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','mention-remove', or'insert-text', 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'.
Attach a ref to MentionsInput when you need to programmatically insert text at the current caret position or replace the current selection:
import { useRef, useState } from 'react'
import { Mention, MentionsInput, type MentionsInputHandle } from 'react-mentions-ts'
const users = [
{ id: 'walter', display: 'Walter White' },
{ id: 'jesse', display: 'Jesse Pinkman' },
]
function ImperativeApiExample() {
const mentionsRef = useRef<MentionsInputHandle>(null)
const [value, setValue] = useState('')
const handleInsertText = () => {
mentionsRef.current?.insertText('anything')
}
return (
<>
<button type="button" onClick={handleInsertText}>
Insert text
</button>
<MentionsInput
ref={mentionsRef}
value={value}
onMentionsChange={({ value: nextValue }) => setValue(nextValue)}
>
<Mention trigger="@" data={users} />
</MentionsInput>
</>
)
}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 (query, { signal, cursor, reason }) |
null |
An array of mentionable entries, or a filtering function that returns matching entries or a cursor page. Async providers receive an AbortSignal so stale requests can be cancelled safely |
| renderSuggestion | function (entry, search, highlightedDisplay, index, focused) | null |
Allows customizing how mention suggestions are rendered (optional) |
| renderEmpty | function (query) => ReactNode |
null |
Renders custom empty-state content when a query completes without any suggestions |
| renderError | function (query, error) => ReactNode |
null |
Renders custom error-state content when an async data provider rejects |
| 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) |
| debounceMs | number | 0 |
Debounces async provider calls to reduce network chatter while typing |
| maxSuggestions | number | unlimited | Caps the number of suggestions rendered from array results. Cursor page results own their page size and are not capped |
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 plus a MentionSearchContext containing an AbortSignal, and should return a promise that resolves with the list of suggestions. When the user keeps typing, the previous request is aborted and its results are ignored automatically.
import type { MentionSearchContext } from 'react-mentions-ts'
type User = { id: string; display: string }
const fetchUsers = async (query: string, { signal }: MentionSearchContext): Promise<User[]> => {
const response = await fetch(`/api/users?search=${query}`, { signal })
return response.json()
}
;<Mention trigger="@" data={fetchUsers} debounceMs={150} maxSuggestions={8} />Async providers can also return cursor pages. The first request receives reason: 'query' and cursor: null. When the overlay list scrolls near the bottom, the provider is called again with reason: 'page' and the previous nextCursor; returned items are appended to the active suggestions. null or undefined nextCursor, or hasMore: false, stops pagination. Providers should avoid duplicate ids within the same query.
type UserPage = {
items: User[]
nextCursor?: string | null
}
const fetchUsersPage = async (
query: string,
{ cursor, signal }: MentionSearchContext
): Promise<UserPage> => {
const params = new URLSearchParams({ search: query })
if (typeof cursor === 'string') {
params.set('cursor', cursor)
}
const response = await fetch(`/api/users?${params.toString()}`, { signal })
return response.json()
}
;<Mention trigger="@" data={fetchUsersPage} debounceMs={150} />Redux-Saga and similar async layers can bridge pagination by returning a promise from data and resolving it from the saga:
const data = (query: string, { cursor, signal }: MentionSearchContext) =>
new Promise<UserPage>((resolve, reject) => {
dispatch(fetchMentionPage({ query, cursor, signal, resolve, reject }))
})The live demo includes many ready-to-use patterns. Each demo's source is in demo/src/examples/:
| Demo | Description |
|---|---|
| Multiple Triggers | Mention teammates with @ or type an email address — styling switches based on the trigger |
| Single Line | Compact single-line input for chat composer bars |
| Multi-Word Queries | Use makeTriggerRegex('@', { allowSpaceInQuery: true }) for multi-word searches |
| Regex Trigger | Fire suggestions on any alphabetical word with no prefix character required |
| Accent-Insensitive | Normalises diacritics before matching so Lydia finds Lydìã |
| Scrollable Composer | Textarea and highlighter stay in sync while scrolling long drafts |
| Auto-Resize | Textarea grows to match its content with autoResize |
| Copy & Paste | Mentions survive clipboard round-trips, even into plain-text fields |
| Caret Mention States | Style mentions based on caret overlap via data-mention-selection attributes |
| Inline Autocomplete | Ghost-text completions accepted with Tab, Enter, or arrow keys |
| Async GitHub Mentions | Live GitHub API search with debouncing, cancellation, and stale-result suppression |
| Emoji Support | Mix people mentions with emoji search powered by a JSON data source |
| Suggestions Portal | Render suggestions anywhere in the DOM for modals, drawers, or fixed toolbars |
| Custom Container | Wrap suggestions in bespoke UI chrome — badges, headlines, or analytics |
| Left Anchored | Pin the overlay to the input's leading edge instead of following the caret |
| Advanced Formatting | Custom markup, programmatic focus, and flipped suggestion lists |
MentionsInput is a controlled component — you must provide a value prop and handle updates via onMentionsChange. The value string uses a markup format to encode mentions inline with plain text.
The default markup template is @[__display__](__id__), so a value containing a mention looks like:
Hey @[Walter White](walter), are you there?
The __display__ placeholder stores the visible text and __id__ stores the mention identifier. When rendered, the user sees plain text (Hey @Walter White, are you there?) while the underlying value preserves the structured mention data.
You can customize the template via the markup prop on Mention, or pass a MentionSerializer for full control (see below).
A utility that builds a properly anchored regex from a trigger string. Useful when you need spaces in queries or accent-insensitive matching without hand-rolling a regex.
import { makeTriggerRegex } from 'react-mentions-ts'
makeTriggerRegex('@')
// default — matches @query at end of string
makeTriggerRegex('@', { allowSpaceInQuery: true })
// allows "@ John Doe" style multi-word queries
makeTriggerRegex('@', { ignoreAccents: true })
// Unicode-aware matching for accented charactersSignature:
makeTriggerRegex(
trigger?: string | RegExp, // default: '@'
options?: {
allowSpaceInQuery?: boolean
ignoreAccents?: boolean // enables the Unicode `u` flag
}
): RegExpWhen trigger is already a RegExp, it is returned as-is.
Converts a markup template string into a MentionSerializer object. Use this when you want custom markup formats without manually wiring up insertion and parsing logic.
import { createMarkupSerializer } from 'react-mentions-ts'
const serializer = createMarkupSerializer(':__id__')
// serializer.insert({ id: 'wave', display: 'Wave' }) → ':wave'
// serializer.findAll('Hello :wave and :smile') → [{ id: 'wave', ... }, { id: 'smile', ... }]Pass the result as the markup prop on Mention:
<Mention trigger=":" data={emojis} markup={createMarkupSerializer(':__id__')} />For cases where createMarkupSerializer is not flexible enough, you can implement the MentionSerializer interface directly:
interface MentionSerializer {
id: string
insert: (input: { id: string | number; display: string }) => string
findAll: (value: string) => MentionSerializerMatch[]
}
interface MentionSerializerMatch {
markup: string // the full matched substring
index: number // position in the value string
id: string // extracted mention identifier
display?: string | null // extracted display text
}id— a unique string identifying this serializer (used internally to distinguish multipleMentionchildren)insert— given a mention'sidanddisplay, returns the markup string to splice into the valuefindAll— scans a value string and returns every mention match with its position and extracted fields
If your mention IDs can contain characters that would otherwise terminate a template placeholder early, write a custom serializer and encode the id before storing it in the markup.
The example below percent-encodes reserved characters so an ID like team)west is stored safely and decoded back to its original value when the controlled value is parsed:
import type { MentionSerializer } from 'react-mentions-ts'
const encodeMentionId = (id: string): string =>
encodeURIComponent(id).replace(
/[!'()*]/g,
(char) => `%${char.charCodeAt(0).toString(16).toUpperCase()}`
)
const decodeMentionId = (encoded: string): string => decodeURIComponent(encoded)
export const parenSafeSerializer: MentionSerializer = {
id: 'paren-safe-serializer',
insert: ({ id, display }) => `@[${display}](${encodeMentionId(String(id))})`,
findAll: (value) => {
const regex = /@\[([^\]]+)]\(([^)]+)\)/g
const matches = []
let match: RegExpExecArray | null
while ((match = regex.exec(value)) !== null) {
matches.push({
markup: match[0],
index: match.index,
display: match[1],
id: decodeMentionId(match[2]),
})
}
return matches
},
}Use it on Mention like any other serializer:
<Mention trigger="@" data={users} markup={parenSafeSerializer} />For example, selecting a mention with id: 'team)west' stores @[Team West](team%29west) in value, and findAll decodes it back to team)west.
The component is SSR-compatible out of the box. It guards against missing browser globals (document) during server rendering, so it works with Next.js, Remix, and other SSR frameworks without extra configuration.
In Next.js App Router, add the 'use client' directive to any file that renders MentionsInput:
'use client'
import { MentionsInput, Mention } from 'react-mentions-ts'No dynamic imports or next/dynamic wrappers are needed.
The package is published as side-effect free, and the release evidence is repeatable:
pnpm tree-shake:reportThe report command rebuilds dist, runs npx publint --pack npm, requires "sideEffects": false, inspects npm pack --dry-run --json, and bundles small Vite consumer fixtures against dist/index.js.
Current verified behavior:
- A fixture that imports only
MentionandgetSubstringIndexmust not retainLoadingIndicator,SuggestionsOverlay, or inline-suggestion markers. - A fixture that imports
MentionsInputcurrently retains overlay, loading, and inline-suggestion markers because those branches are statically imported by the orchestration shell. - The npm pack check prints the tarball filename, packed size, unpacked size, and file count so publish contents stay visible.
Maintainer follow-up opportunities are LoadingIndicator, SuggestionsOverlay, and the inline-suggestion UI/selector path. Those can be split or lazy-loaded in a future packaging change for consumers that never use those branches. This is not a consumer setup requirement; apps can keep importing MentionsInput normally, including in SSR frameworks, without next/dynamic wrappers.
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.
Assign a className prop to MentionsInput. All DOM nodes will receive derived class names:
<MentionsInput className="mentions">
<Mention className="mentions__mention" />
</MentionsInput><MentionsInput style={customStyle}>
<Mention style={mentionStyle} />
</MentionsInput>Every rendered mention 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 purely in CSS 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" is set, the component renders a ghost-text hint next to the caret instead of a dropdown overlay. The user accepts the suggestion with Tab, Enter, or the right arrow key.
<MentionsInput
value={value}
onMentionsChange={({ value: nextValue }) => setValue(nextValue)}
suggestionsDisplay="inline"
>
<Mention trigger="@" data={users} />
</MentionsInput>Customize the inline hint appearance via the classNames prop:
| Class name slot | What it targets |
|---|---|
inlineSuggestion |
The absolutely-positioned wrapper around the hint |
inlineSuggestionText |
The visible completion text |
inlineSuggestionPrefix |
The already-typed portion (hidden with sr-only) |
inlineSuggestionSuffix |
The remaining suggestion shown after the caret |
See demo/src/examples/defaultStyle.ts for a full styling example.
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...
})My Tailwind classes aren't applying to the library's DOM.
Tailwind v4 only generates utilities it finds in your source. Either import react-mentions-ts/styles/tailwind.css (which adds an @source pointing at the library's dist), or on Tailwind v3 add ./node_modules/react-mentions-ts/dist/**/*.{js,jsx,ts,tsx} to your content array. See Tailwind CSS.
The caret or highlighted mentions look misaligned inside a scroll container.
The overlay and textarea must share the same font metrics, padding, and line-height. If you wrap MentionsInput in a scrollable element, avoid changing font size or box-sizing on an intermediate wrapper; apply styling directly to MentionsInput via className or style. The "Scrollable Composer" demo shows a working setup.
I'm getting a "document is not defined" error during SSR.
In Next.js App Router, add 'use client' to any file that imports MentionsInput. The component itself guards against missing browser globals, but the module must only execute on the client for event listeners to attach correctly.
My custom trigger RegExp isn't matching.
Three rules: (1) do not include the global /g flag — the internal clone shares lastIndex and will skip matches; (2) anchor with $ so it only matches at the cursor position; (3) expose exactly two capture groups — the first for trigger + query (e.g. @mention), the second for the query alone (e.g. mention). If you just need spaces or accent-insensitivity, use makeTriggerRegex instead of rolling your own.
Mention IDs containing ) or ] corrupt my markup.
The default template @[__display__](__id__) uses ) as a terminator. Either switch to a template that can't collide with your IDs, or implement a custom MentionSerializer that encodes reserved characters. See Custom serializer for IDs containing ).
Async requests aren't cancelling.
You must forward the signal from MentionSearchContext into fetch (or your HTTP client's equivalent). Without it, stale responses will race and overwrite the active query's results.
onBlur isn't firing with the expected signature.
The native onBlur is unchanged. For the library-specific callback that also reports whether focus moved to a suggestion, use onMentionBlur(event, clickedSuggestion).
This library is a TypeScript rewrite of react-mentions. The API has been modernized — most changes are mechanical renames, but a few features work differently.
- npm install react-mentions
+ npm install react-mentions-ts
- import { MentionsInput, Mention } from 'react-mentions'
+ import { MentionsInput, Mention } from 'react-mentions-ts'The package ships a jscodeshift transform that handles most of the mechanical renames. After installing react-mentions-ts, run it against your source tree:
npx jscodeshift \
--parser=tsx \
--extensions=tsx,ts,jsx,js \
--transform=node_modules/react-mentions-ts/codemods/react-mentions-to-react-mentions-ts.cjs \
srcWhat the codemod rewrites automatically:
- Updates ESM
importand CommonJSrequirestatements (including namespace and destructured forms) fromreact-mentionstoreact-mentions-ts. - Renames
onChange→onMentionsChangeonMentionsInput. Inline arrow/function handlers have their positional parameters (event,newValue,newPlainTextValue,mentions) converted to the object payload; if the handler body references the event parameter, aconst event = trigger.nativeEventalias is prepended so existing code keeps working. - Renames
onBlur→onMentionBluronMentionsInput. - Rewrites
onAddonMentionfrom positional arguments (id,display,startPos,endPos) to the object payload. - Consolidates
allowSuggestionsAboveCursor/forceSuggestionsAboveCursorintosuggestionsPlacement="auto"/"above". - Removes
allowSpaceInQueryandignoreAccentsfromMentionsInputand moves them onto each childMention'striggerviamakeTriggerRegex('@', { … }), auto-importing the helper. - Removes the separate
regexprop and wraps staticmarkupstrings withcreateMarkupSerializer(...), auto-importing the helper. - When
onChangeoronAddis passed as a bare identifier or member expression (e.g.onChange={this.handleChange}), wraps it in an adapter that calls the existing handler with the legacy positional arguments so the original implementation keeps working until you migrate it by hand.
The following cases are reported via api.report(...) (printed by jscodeshift) and must be migrated by hand:
dataproviders that use the legacy(query, callback) => …signature must be rewritten to return an array or a promise (the codemod flags inlinedatafunctions that declare two or more parameters so you can confirm).- Dynamic values for
allowSpaceInQuery,ignoreAccents,allowSuggestionsAboveCursor, orforceSuggestionsAboveCursor(e.g.allowSpaceInQuery={someFlag}) — these are removed with a warning and the equivalent trigger regex must be wired up manually. Mentionelements usingregexwhenmarkupis missing or dynamic.- Callback attributes whose value is not an inline function, identifier, or member expression.
Review the diff and run your test suite after applying it.
| react-mentions | react-mentions-ts | Notes |
|---|---|---|
onChange(event, newValue, newPlainTextValue, mentions) |
onMentionsChange({ trigger, value, plainTextValue, idValue, mentionId, mentions, previousValue }) |
Receives a single object instead of positional arguments. See onMentionsChange payload for details |
onBlur(event, clickedSuggestion) |
onMentionBlur(event, clickedSuggestion) |
Renamed to avoid shadowing the native onBlur (which is also available) |
allowSuggestionsAboveCursor |
suggestionsPlacement="auto" |
Use 'auto', 'above', or 'below' instead of two separate booleans |
forceSuggestionsAboveCursor |
suggestionsPlacement="above" |
|
allowSpaceInQuery (on MentionsInput) |
trigger={makeTriggerRegex('@', { allowSpaceInQuery: true })} (on Mention) |
Moved from a top-level boolean to a per-trigger option via the makeTriggerRegex utility |
onAdd(id, display, startPos, endPos) |
onAdd({ id, display, startPos, endPos, serializerId }) |
Receives a single object; adds serializerId |
| react-mentions | react-mentions-ts | Notes |
|---|---|---|
markup (string) + regex |
markup (string | MentionSerializer) |
The separate regex prop is removed. Pass a MentionSerializer for custom parsing, or use createMarkupSerializer(template) to convert a legacy template string. See MentionSerializer Interface |
| Feature | Prop / API |
|---|---|
Async data via Promises with AbortSignal |
data accepts (query, { signal, cursor, reason }) => Promise<...> |
| Cursor-paginated async suggestions | Return { items, nextCursor, hasMore } from data |
| Debounced async queries | debounceMs on Mention |
| Cap suggestion count | maxSuggestions on Mention |
| Caret-aware mention styling | onMentionSelectionChange, data-mention-selection attribute |
| Inline ghost-text autocomplete | suggestionsDisplay="inline" |
| Auto-resizing textarea | autoResize |
| Left-anchored overlay | anchorMode="left" |
| Empty/error state rendering | renderEmpty, renderError on Mention |
| Tailwind v4 styling out of the box | Built-in utility classes |
react-mentions shipped no CSS and relied entirely on inline styles or manual class names. react-mentions-ts ships with Tailwind utility classes baked in. If you were using inline style props, they still work. If you were using className / classNames, those work too. See the Styling section for setup.
<MentionsInput
value={value}
- onChange={(e, newValue) => setValue(newValue)}
+ onMentionsChange={({ value: nextValue }) => setValue(nextValue)}
- allowSuggestionsAboveCursor
+ suggestionsPlacement="auto"
>
<Mention
trigger="@"
data={users}
- markup="@[__display__](__id__)"
- regex={/@\[(.+?)]\((.+?)\)/}
+ markup={createMarkupSerializer('@[__display__](__id__)')}
/>
</MentionsInput>The default markup template
@[__display__](__id__)is unchanged, so if you were using the default you can omit themarkupprop entirely — nocreateMarkupSerializercall needed.
Pull requests, bug reports, and feature suggestions are welcome. See CONTRIBUTING.md for development setup (Node 22+, Corepack, pnpm install), test instructions, and the PR checklist. All contributors are expected to follow the Code of Conduct.
This project is a TypeScript rewrite and modernization of the original react-mentions library.
Made with contrib.rocks.