The documentation currently represents the 2.0.0-rc8 release.
For troubleshooting please checkout theFAQ

Mention

Mentions for everyone! This plugin allows the user to choose an entry from a list. After selection an entry the search text will be replace with the selected entity. The list of suggestions mentions needs to contain at least a name to display. If desired a link and/or an avatar image can be provided.

Escape Behaviour

While the suggestion popover is open, the user can close it by pressing ESC. This will be stored for as long as the the selection stays inside the word that triggered the search. After the selection left this word once the escape behaviour will be reset. The suggestions will appear again once the user selects the word that that triggered the selection.

Supported Environment

  • Desktop: Yes
  • Mobile: Yes
  • Screen-reader: Yes

Getting Started

npm install [email protected] --save
npm install [email protected] --save
Please checkout the 'Simple Example' further down the page.

Importing the default styles

The plugin ships with a default styling available at this location in the installed package:  node_modules/draft-js-mention-plugin/lib/plugin.css

Webpack Usage

  • 1. Install Webpack loaders:  npm i style-loader css-loader --save-dev
  • 2. Add the below section to Webpack config (if your config already has a loaders array, simply add the below loader object to your existing list.
    module.exports = {
      module: {
        loaders: [
          {
            test: /plugin\.css$/,
            loaders: [
              'style-loader', 'css',
            ],
          },
        ],
      },
    };
    
  • 3. Add the below import line to your component to tell Webpack to inject the style to your component.
    import 'draft-js-mention-plugin/lib/plugin.css';
    
  • 4. Restart Webpack.

Browserify Usage

Please help, by submiting a Pull Request to the documentation.

Configuration Parameters

themeObject of CSS classes with the following keys.
mention:CSS class for mention text.
mentionSuggestions:CSS class for suggestions component.
mentionSuggestionsEntry:CSS class for an entry in the suggestions component.
mentionSuggestionsEntryFocused:CSS class for the focused entry in the suggestions component.
mentionSuggestionsEntryText:CSS class for an entry’s text in the suggestions component.
mentionSuggestionsEntryAvatar:CSS class for an entry’s avatar image in the suggestions component.
positionSuggestionsThe function can be used to manipulate the position of the popover containing the suggestions. It receives one object as arguments containing the visible rectangle surrounding the decorated search string including the @. In addition the object contains prevProps, prevState, state & props. An object should be returned which can contain all sorts of styles. The defined properties will be applied as inline-styles.
entityMutabilityCan be one of: "IMMUTABLE", "SEGMENTED" or "MUTABLE". Read in detail about it here
mentionPrefixBy default it is an empty String. For Twitter or Slack like mention behaviour you can provide an `@`
mentionTriggerAllows you to provide a custom character to change when the search is triggered. By default it is set to `@`. By default typing `@` will trigger the search for mentions. Note: the implementation does not support a multi-character mentionTrigger.
mentionRegExpAllows you to overwrite the regular expression for initiating the dropdown. By default this supports any alphanumeric character as well as Chinese, Japanese & Korean characters. We are happy to accept pull requests to extend the default mentionRegExp as well.
mentionComponentIf provided the passed component is used to render a Mention. It receives the following props: entityKey, mention, className & decoratedText

MentionSuggestions

The MentionSuggestions component is part of the plugin and should placed somewhere in the JSX after the Editor. It takes the following props:
onSearchChangeA callback which is triggered whenever the search term changes. The first argument is an object which constains the search term in the property value.
suggestionsThe list of suggestions to be shown.
onOpenA callback which is triggered whenever the suggestions popover opens.
onCloseA callback which is triggered whenever the suggestions popover closes.
entryComponentComponent to be used as the template for each of the suggestions entry.
onAddMentionA callback which is triggered whenever the mention is about to be added. The first argument of this callback will contain the mention entry.
popoverComponentComponent to be used as the template for the popover (the parent of entryComponent). Defaults to a div.
mentionSuggestionsComponentComponent to be used to render the suggestions dropdown. It must implement the same interface like MentionSuggestions. Defaults to MentionSuggestions.
Additional properties are passed to the popoverComponent

Additional Exports

In addition to the plugin the module exports `defaultSuggestionsFilter`. As first argument it takes the search term as a String. The second argument is an array of mentions. The function returns the filter list based on substring matches.
import { defaultSuggestionsFilter } from 'draft-js-mention-plugin';

Simple Example

SimpleMentionEditor.js
import React, { Component } from 'react';
import { EditorState } from 'draft-js';
import Editor from 'draft-js-plugins-editor';
import createMentionPlugin, { defaultSuggestionsFilter } from 'draft-js-mention-plugin';
import editorStyles from './editorStyles.css';
import mentions from './mentions';

export default class SimpleMentionEditor extends Component {

  constructor(props) {
    super(props);

    this.mentionPlugin = createMentionPlugin();
  }

  state = {
    editorState: EditorState.createEmpty(),
    suggestions: mentions,
  };

  onChange = (editorState) => {
    this.setState({
      editorState,
    });
  };

  onSearchChange = ({ value }) => {
    this.setState({
      suggestions: defaultSuggestionsFilter(value, mentions),
    });
  };

  onAddMention = () => {
    // get the mention object selected
  }

  focus = () => {
    this.editor.focus();
  };

  render() {
    const { MentionSuggestions } = this.mentionPlugin;
    const plugins = [this.mentionPlugin];

    return (
      <div className={editorStyles.editor} onClick={this.focus}>
        <Editor
          editorState={this.state.editorState}
          onChange={this.onChange}
          plugins={plugins}
          ref={(element) => { this.editor = element; }}
        />
        <MentionSuggestions
          onSearchChange={this.onSearchChange}
          suggestions={this.state.suggestions}
          onAddMention={this.onAddMention}
        />
      </div>
    );
  }
}
mentions.js
const mentions = [
  {
    name: 'Matthew Russell',
    link: 'https://twitter.com/mrussell247',
    avatar: 'https://pbs.twimg.com/profile_images/517863945/mattsailing_400x400.jpg',
  },
  {
    name: 'Julian Krispel-Samsel',
    link: 'https://twitter.com/juliandoesstuff',
    avatar: 'https://avatars2.githubusercontent.com/u/1188186?v=3&s=400',
  },
  {
    name: 'Jyoti Puri',
    link: 'https://twitter.com/jyopur',
    avatar: 'https://avatars0.githubusercontent.com/u/2182307?v=3&s=400',
  },
  {
    name: 'Max Stoiber',
    link: 'https://twitter.com/mxstbr',
    avatar: 'https://pbs.twimg.com/profile_images/763033229993574400/6frGyDyA_400x400.jpg',
  },
  {
    name: 'Nik Graf',
    link: 'https://twitter.com/nikgraf',
    avatar: 'https://avatars0.githubusercontent.com/u/223045?v=3&s=400',
  },
  {
    name: 'Pascal Brandt',
    link: 'https://twitter.com/psbrandt',
    avatar: 'https://pbs.twimg.com/profile_images/688487813025640448/E6O6I011_400x400.png',
  },
];

export default mentions;
editorStyles.css
.editor {
  box-sizing: border-box;
  border: 1px solid #ddd;
  cursor: text;
  padding: 16px;
  border-radius: 2px;
  margin-bottom: 2em;
  box-shadow: inset 0px 1px 8px -3px #ABABAB;
  background: #fefefe;
}

.editor :global(.public-DraftEditor-content) {
  min-height: 140px;
}

Custom Themed Mention Example

CustomMentionEditor.js
import React, { Component } from 'react';
import { EditorState } from 'draft-js';
import Editor from 'draft-js-plugins-editor';
import createMentionPlugin, { defaultSuggestionsFilter } from 'draft-js-mention-plugin';
import editorStyles from './editorStyles.css';
import mentionsStyles from './mentionsStyles.css';
import mentions from './mentions';

const positionSuggestions = ({ state, props }) => {
  let transform;
  let transition;

  if (state.isActive && props.suggestions.size > 0) {
    transform = 'scaleY(1)';
    transition = 'all 0.25s cubic-bezier(.3,1.2,.2,1)';
  } else if (state.isActive) {
    transform = 'scaleY(0)';
    transition = 'all 0.25s cubic-bezier(.3,1,.2,1)';
  }

  return {
    transform,
    transition,
  };
};

const Entry = (props) => {
  const {
    mention,
    theme,
    searchValue, // eslint-disable-line no-unused-vars
    ...parentProps
  } = props;

  return (
    <div {...parentProps}>
      <div className={theme.mentionSuggestionsEntryContainer}>
        <div className={theme.mentionSuggestionsEntryContainerLeft}>
          <img
            src={mention.get('avatar')}
            className={theme.mentionSuggestionsEntryAvatar}
            role="presentation"
          />
        </div>

        <div className={theme.mentionSuggestionsEntryContainerRight}>
          <div className={theme.mentionSuggestionsEntryText}>
            {mention.get('name')}
          </div>

          <div className={theme.mentionSuggestionsEntryTitle}>
            {mention.get('title')}
          </div>
        </div>
      </div>
    </div>
  );
};

export default class CustomMentionEditor extends Component {

  constructor(props) {
    super(props);

    this.mentionPlugin = createMentionPlugin({
      mentions,
      entityMutability: 'IMMUTABLE',
      theme: mentionsStyles,
      positionSuggestions,
      mentionPrefix: '@',
    });
  }

  state = {
    editorState: EditorState.createEmpty(),
    suggestions: mentions,
  };

  onChange = (editorState) => {
    this.setState({
      editorState,
    });
  };

  onSearchChange = ({ value }) => {
    this.setState({
      suggestions: defaultSuggestionsFilter(value, mentions),
    });
  };

  focus = () => {
    this.editor.focus();
  };

  render() {
    const { MentionSuggestions } = this.mentionPlugin;
    const plugins = [this.mentionPlugin];

    return (
      <div className={editorStyles.editor} onClick={this.focus}>
        <Editor
          editorState={this.state.editorState}
          onChange={this.onChange}
          plugins={plugins}
          ref={(element) => { this.editor = element; }}
        />
        <MentionSuggestions
          onSearchChange={this.onSearchChange}
          suggestions={this.state.suggestions}
          entryComponent={Entry}
        />
      </div>
    );
  }
}
mentionsStyles.js
.mention {
  color: #4a85bb;
  text-decoration: none;
}

.mentionSuggestions {
  border-top: 1px solid #eee;
  background: #fff;
  border-radius: 2px;
  cursor: pointer;
  padding-top: 8px;
  padding-bottom: 8px;
  display: flex;
  flex-direction: column;
  box-sizing: border-box;
  transform-origin: 50% 0%;
  transform: scaleY(0);
  margin: -16px;
}

.mentionSuggestionsEntryContainer {
  display: table;
  width: 100%;
}

.mentionSuggestionsEntryContainerLeft,
.mentionSuggestionsEntryContainerRight {
  display: table-cell;
  vertical-align: middle;
}

.mentionSuggestionsEntryContainerRight {
  width: 100%;
  padding-left: 8px;
}

.mentionSuggestionsEntry {
  padding: 7px 10px 3px 10px;
  transition: background-color 0.4s cubic-bezier(.27,1.27,.48,.56);
}

.mentionSuggestionsEntry:active {
  background-color: #cce7ff;
}

.mentionSuggestionsEntryFocused {
  composes: mentionSuggestionsEntry;
  background-color: #e6f3ff;
}

.mentionSuggestionsEntryText,
.mentionSuggestionsEntryTitle {
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.mentionSuggestionsEntryText {
}

.mentionSuggestionsEntryTitle {
  font-size: 80%;
  color: #a7a7a7;
}

.mentionSuggestionsEntryAvatar {
  display: block;
  width: 30px;
  height: 30px;
  border-radius: 50%;
}
mentions.js
import { fromJS } from 'immutable';

const mentions = fromJS([
  {
    name: 'matthew',
    title: 'Senior Software Engineer',
    avatar: 'https://pbs.twimg.com/profile_images/517863945/mattsailing_400x400.jpg',
  },
  {
    name: 'julian',
    title: 'United Kingdom',
    avatar: 'https://avatars2.githubusercontent.com/u/1188186?v=3&s=400',
  },
  {
    name: 'jyoti',
    title: 'New Delhi, India',
    avatar: 'https://avatars0.githubusercontent.com/u/2182307?v=3&s=400',
  },
  {
    name: 'max',
    title: 'Travels around the world, brews coffee, skis mountains and makes stuff on the web.',
    avatar: 'https://pbs.twimg.com/profile_images/763033229993574400/6frGyDyA_400x400.jpg',
  },
  {
    name: 'nik',
    title: 'Passionate about Software Architecture, UX, Skiing & Triathlons',
    avatar: 'https://avatars0.githubusercontent.com/u/223045?v=3&s=400',
  },
  {
    name: 'pascal',
    title: 'HeathIT hacker and researcher',
    avatar: 'https://pbs.twimg.com/profile_images/688487813025640448/E6O6I011_400x400.png',
  },
]);

export default mentions;
editorStyles.css
.editor {
  box-sizing: border-box;
  border: 1px solid #ddd;
  cursor: text;
  padding: 16px;
  border-radius: 2px;
  margin-bottom: 2em;
  box-shadow: inset 0px 1px 8px -3px #ABABAB;
  background: #fefefe;
}

.editor :global(.public-DraftEditor-content) {
  min-height: 140px;
}

Remote Data Mention Example

RemoteMentionEditor.js
import React, { Component } from 'react';
import { EditorState } from 'draft-js';

import Editor from 'draft-js-plugins-editor';

import createMentionPlugin from 'draft-js-mention-plugin';
import { fromJS } from 'immutable';
import editorStyles from './editorStyles.css';

export default class SimpleMentionEditor extends Component {

  constructor(props) {
    super(props);

    this.mentionPlugin = createMentionPlugin();
  }

  state = {
    editorState: EditorState.createEmpty(),
    suggestions: fromJS([]),
  };

  onChange = (editorState) => {
    this.setState({
      editorState,
    });
  };

  onSearchChange = ({ value }) => {
    // An import statment would break server-side rendering.
    require('whatwg-fetch'); // eslint-disable-line global-require

    // while you normally would have a dynamic server that takes the value as
    // a workaround we use this workaround to show different results
    let url = '/data/mentionsA.json';
    if (value.length === 1) {
      url = '/data/mentionsB.json';
    } else if (value.length > 1) {
      url = '/data/mentionsC.json';
    }

    fetch(url)
      .then((response) => response.json())
      .then((data) => {
        this.setState({
          suggestions: fromJS(data),
        });
      });
  };

  focus = () => {
    this.editor.focus();
  };

  render() {
    const { MentionSuggestions } = this.mentionPlugin;
    const plugins = [this.mentionPlugin];

    return (
      <div className={editorStyles.editor} onClick={this.focus}>
        <Editor
          editorState={this.state.editorState}
          onChange={this.onChange}
          plugins={plugins}
          ref={(element) => { this.editor = element; }}
        />
        <MentionSuggestions
          onSearchChange={this.onSearchChange}
          suggestions={this.state.suggestions}
        />
      </div>
    );
  }
}
editorStyles.css
.editor {
  box-sizing: border-box;
  border: 1px solid #ddd;
  cursor: text;
  padding: 16px;
  border-radius: 2px;
  margin-bottom: 2em;
  box-shadow: inset 0px 1px 8px -3px #ABABAB;
  background: #fefefe;
}

.editor :global(.public-DraftEditor-content) {
  min-height: 140px;
}

Custom Mention Component Example

CustomComponentMentionEditor.js
import React, { Component } from 'react';
import { EditorState } from 'draft-js';
import Editor from 'draft-js-plugins-editor';
import createMentionPlugin, { defaultSuggestionsFilter } from 'draft-js-mention-plugin';
import editorStyles from './editorStyles.css';
import mentions from './mentions';

export default class CustomMentionEditor extends Component {

  constructor(props) {
    super(props);

    this.mentionPlugin = createMentionPlugin({
      mentions,
      mentionComponent: (mentionProps) => (
        <span
          className={mentionProps.className}
          // eslint-disable-next-line no-alert
          onClick={() => alert('Clicked on the Mention!')}
        >
          {mentionProps.children}
        </span>
      ),
    });
  }

  state = {
    editorState: EditorState.createEmpty(),
    suggestions: mentions,
  };

  onChange = (editorState) => {
    this.setState({
      editorState,
    });
  };

  onSearchChange = ({ value }) => {
    this.setState({
      suggestions: defaultSuggestionsFilter(value, mentions),
    });
  };

  focus = () => {
    this.editor.focus();
  };

  render() {
    const { MentionSuggestions } = this.mentionPlugin;
    const plugins = [this.mentionPlugin];

    return (
      <div className={editorStyles.editor} onClick={this.focus}>
        <Editor
          editorState={this.state.editorState}
          onChange={this.onChange}
          plugins={plugins}
          ref={(element) => { this.editor = element; }}
        />
        <MentionSuggestions
          onSearchChange={this.onSearchChange}
          suggestions={this.state.suggestions}
        />
      </div>
    );
  }
}
editorStyles.css
.editor {
  box-sizing: border-box;
  border: 1px solid #ddd;
  cursor: text;
  padding: 16px;
  border-radius: 2px;
  margin-bottom: 2em;
  box-shadow: inset 0px 1px 8px -3px #ABABAB;
  background: #fefefe;
}

.editor :global(.public-DraftEditor-content) {
  min-height: 140px;
}