Lightweight @-mention autocomplete for <textarea> and contenteditable.
No dependencies. ~7 KB gzipped. TypeScript definitions included.
- Works with both
<textarea>andcontenteditableelements - Async search function with debounce and stale-request guard
- Scroll-based and keyboard-based pagination via
nextPageUrl - Keyboard navigation: Arrow keys, Enter, Tab, Escape
- Programmatic API:
push(),getMentions(),clear(),destroy() - Avatar support (image URL or auto-generated letter placeholder)
- Viewport-aware dropdown positioning (flips above cursor when near bottom)
- Animated dropdown appearance (CSS transition)
- UMD module format (browser global, CommonJS, AMD)
<link rel="stylesheet" href="mention.css">
<script src="mention.js"></script>
<div id="editor" contenteditable="true"></div>
<script>
const mention = new MentionJS(document.getElementById('editor'), {
trigger: '@',
debounceDelay: 300,
noResultsText: 'Not found',
searchFunction: async (query, nextPageUrl) => {
const url = nextPageUrl || `/api/users?q=${encodeURIComponent(query)}`;
const res = await fetch(url);
return await res.json();
// Expected: { items: [{ id, name, avatar?, details? }], nextPageUrl: string | null }
},
onMentionSelect(data) {
console.log('Selected:', data.id, data.name);
},
});
</script>npm install @shelamkoff/mentionjs// CommonJS
const MentionJS = require('@shelamkoff/mentionjs');// ES Module (with bundler)
import MentionJS from '@shelamkoff/mentionjs';CDN:
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@shelamkoff/mentionjs/dist/mention.min.css">
<script src="https://cdn.jsdelivr.net/npm/@shelamkoff/mentionjs/dist/mention.min.js"></script>Manual:
<link rel="stylesheet" href="mention.css">
<script src="mention.js"></script>const m = new MentionJS(element, options);element must be a <textarea> or an element with contenteditable="true".
| Option | Type | Default | Description |
|---|---|---|---|
trigger |
string |
'@' |
Character that opens the dropdown |
searchFunction |
SearchFunction |
null |
Async search function (see below) |
debounceDelay |
number |
300 |
Debounce delay in ms for non-empty queries |
noResultsText |
string |
'No results found' |
Text shown when search returns no items |
dropdownClass |
string |
'' |
Additional CSS class for the dropdown container |
onMentionSelect |
(data: { id, name }) => void |
null |
Callback when a mention is committed |
renderItem |
(data, index, isActive) => HTMLElement |
null |
Custom render function for dropdown items |
renderNoResults |
(noResultsText) => HTMLElement |
null |
Custom render function for the "no results" row |
renderLoading |
() => HTMLElement |
null |
Custom render function for the loading indicator |
type SearchFunction = (
query: string,
nextPageUrl?: string | null
) => Promise<SearchResult | MentionItem[]>;Must return a Promise resolving to:
{
items: [
{ id: 1, name: 'Alice', avatar: '/img/alice.jpg', details: 'Developer' },
{ id: 2, name: 'Bob', details: 'Designer' },
],
nextPageUrl: '/api/users?q=a&page=2' // null when no more pages
}items[].id— unique identifier (string or number)items[].name— display name (required)items[].avatar— image URL (optional; letter placeholder is generated when absent)items[].details— secondary text line (optional)nextPageUrl— URL for the next page;nullmeans no more pages
You may also return a plain array of items (without pagination).
Empty-string queries (query === '') are executed immediately (no debounce) to show the initial list when the trigger character is typed.
All render functions are optional. When provided, they must return an HTMLElement. If they return a falsy value, the default rendering is used as a fallback.
renderItem(data, index, isActive)
Custom rendering for each dropdown item. The returned element automatically gets mention-item class and data-index attribute.
renderItem(data, index, isActive) {
const el = document.createElement('div');
el.className = 'mention-item' + (isActive ? ' mention-active' : '');
el.innerHTML = `
<img src="${data.avatar}" class="mention-avatar">
<div class="mention-info">
<div class="mention-name">${data.name}</div>
<span class="badge">${data.role}</span>
</div>
`;
return el;
}renderNoResults(noResultsText)
Custom rendering for the empty-results state.
renderNoResults(text) {
const el = document.createElement('div');
el.className = 'mention-item mention-no-results';
el.textContent = text;
return el;
}renderLoading()
Custom rendering for the pagination loading indicator. The returned element automatically gets mention-loading class.
renderLoading() {
const el = document.createElement('div');
el.className = 'mention-loading';
el.innerHTML = '<div class="mention-item"><div class="spinner"></div></div>';
return el;
}Returns an array of all committed mentions.
Textarea returns objects with character offsets:
[{ id: 1, name: 'Alice', start: 0, end: 6 }]ContentEditable returns id and name only:
[{ id: '1', name: 'Alice' }]Programmatically inserts a mention at the current cursor position, or at the end of the field if no cursor is active.
m.push({ id: 1, name: 'Alice' });Clears all content and committed mentions.
Removes all event listeners and the dropdown element. Call before removing the host element from the DOM.
Static factory method. Equivalent to new MentionJS(element, options).
Import mention.css for default styles. All classes are customizable:
| Class | Description |
|---|---|
.mention-dropdown |
Dropdown container (positioned absolute, appended to <body>) |
.mention-dropdown.active |
Visible state (opacity 1, pointer-events auto) |
.mention-item |
Individual item row |
.mention-item.mention-active |
Highlighted item (keyboard or hover) |
.mention-item.mention-no-results |
"No results" row |
.mention-avatar |
Avatar <img> element |
.mention-avatar-placeholder |
Letter-circle fallback avatar |
.mention-info |
Text container (name + details) |
.mention-name |
Primary name text |
.mention-details |
Secondary details text |
.mention-loading |
Loading indicator row (pagination) |
.mention |
Committed mention <span> inside contenteditable |
.mention.active |
Active (being edited) mention span |
Textarea: Mentions are tracked as { id, name, start, end } objects. Positions are recalculated on every input via _syncMentionPositions(). The mention text is displayed inline as @Name.
ContentEditable: Each mention is a <span class="mention"> with data-mention-id and data-mention-name attributes. Active (in-progress) mentions have the .active class. All input inside mention spans is intercepted via the beforeinput event for full control over editing behavior.
| File | Description |
|---|---|
mention.js |
Library source (~1220 lines) |
mention.css |
Default stylesheet |
mention.d.ts |
TypeScript type definitions |
dist/mention.min.js |
Minified JS (~22 KB) |
dist/mention.min.css |
Minified CSS (~2 KB) |
demo.html |
Interactive demo page |
Requires beforeinput event support (all modern browsers). No IE11 support.
MIT