Triggered @mention Autocomplete like Facebook, Twitter & Google+

By Hawkee on Mar 22, 2012


This widget lets you search for users to @mention in your posts. It works very much like Facebook and Google+ in that it supports users with spaces in their name. It writes to a hidden field with the user ID's formatted in this way: @ [12345] while showing @username in the input box. You can save the encoded string for easier parsing at display time.

    hidden: '#hidden_inputbox',
    source: "/search.php",
    trigger: "@" 

You can use a predefined array or json as a source. Example json result:


To use the hidden field without an ajax call you need to pass an associative array:

    hidden: '#hidden_inputbox,
    source: new Array({ "value": "1234", "label": 'Geech'}, {"value": "5312", "label": "Marf"})

This also supports an optional img to appear beside each result. You just need to pass an img URL for each value and label. Here is the CSS for the image:

.ui-menu-item img { padding-right: 10px; width: 32px; height: 32px; }
.ui-menu-item span { color: #444; font-size: 12px; vertical-align: top }

If you want editable posts, you need to pass an id_map as an attr tag of the input box. This is also json encoded and is simply an associative array of the included user_id => username pairs in the existing post. This is so when you change the post the original @mentions are preserved in their @ [12345] format.

Live Demo:

GitHub repo:

Node.js server side component:

 * triggeredAutocomplete (jQuery UI autocomplete widget)
 * 2012 by (
 * Version 1.4.5
 * Requires jQuery 1.7 and jQuery UI 1.8
 * Dual licensed under MIT or GPLv2 licenses

;(function ( $, window, document, undefined ) {
    $.widget("ui.triggeredAutocomplete", $.extend(true, {}, $.ui.autocomplete.prototype, {

        options: {
            trigger: "@",
            allowDuplicates: true

        _create:function() {

            var self = this;
            this.id_map = new Object();
            this.stopIndex = -1;
            this.stopLength = -1;
            this.contents = '';
            this.cursorPos = 0;

            /** Fixes some events improperly handled by ui.autocomplete */
            this.element.bind('keydown.autocomplete.fix', function (e) {
                switch (e.keyCode) {
                    case $.ui.keyCode.ESCAPE:
                    case $.ui.keyCode.UP:
                    case $.ui.keyCode.DOWN:
                        if (!":visible")) {

            // Check for the id_map as an attribute.  This is for editing.

            var id_map_string = this.element.attr('id_map');
            if(id_map_string) this.id_map = jQuery.parseJSON(id_map_string);

   = $.ui.autocomplete.prototype;
  , arguments);


            // Select function defined via options.
   = function(event, ui) {
                var contents = self.contents;
                var cursorPos = self.cursorPos;

                // Save everything following the cursor (in case they went back to add a mention)
                // Separate everything before the cursor
                // Remove the trigger and search
                // Rebuild: start + result + end

                var end = contents.substring(cursorPos, contents.length);
                var start = contents.substring(0, cursorPos);
                start = start.substring(0, start.lastIndexOf(self.options.trigger));

                var top = self.element.scrollTop();
                this.value = start + self.options.trigger+ui.item.label+' ' + end;

                // Create an id map so we can create a hidden version of this string with id's instead of labels.

                self.id_map[ui.item.label] = ui.item.value;

                /** Places the caret right after the inserted item. */
                var index = start.length + self.options.trigger.length + ui.item.label.length + 2;
                if (this.createTextRange) {
                    var range = this.createTextRange();
                    range.move('character', index);
                } else if (this.setSelectionRange) {
                    this.setSelectionRange(index, index);

                return false;

            // Don't change the input as you browse the results.
            this.options.focus = function(event, ui) { return false; }
   = function(event, ui) { return false; }

            // Any changes made need to update the hidden field.
            this.element.focus(function() { self.updateHidden(); });
            this.element.change(function() { self.updateHidden(); });

        // If there is an 'img' then show it beside the label.

        _renderItem:  function( ul, item ) {
            if(item.img != undefined) {
                return $( "<li></li>" )
                    .data( "item.autocomplete", item )
                    .append( "<a>" + "<img src='" + item.img + "' /><span>"+item.label+"</span></a>" )
                    .appendTo( ul );
            else {  
                return $( "<li></li>" )
                    .data( "item.autocomplete", item )
                    .append( $( "<a></a>" ).text( item.label ) )
                    .appendTo( ul );

        // This stops the input box from being cleared when traversing the menu.

        _move: function( direction, event ) {
            if ( !":visible") ) {
       null, event );
            if ( && /^previous/.test(direction) ||
           && /^next/.test(direction) ) {
  [ direction ]( event );

        search: function(value, event) {

            var contents = this.element.val();
            var cursorPos = this.getCursor();
            this.contents = contents;
            this.cursorPos = cursorPos;

            // Include the character before the trigger and check that the trigger is not in the middle of a word
            // This avoids trying to match in the middle of email addresses when '@' is used as the trigger

            var check_contents = contents.substring(contents.lastIndexOf(this.options.trigger) - 1, cursorPos);
            var regex = new RegExp('\\B\\'+this.options.trigger+'([\\w\\-]+)');

            if (contents.indexOf(this.options.trigger) >= 0 && check_contents.match(regex)) {

                // Get the characters following the trigger and before the cursor position.
                // Get the contents up to the cursortPos first then get the lastIndexOf the trigger to find the search term.

                contents = contents.substring(0, cursorPos);
                var term = contents.substring(contents.lastIndexOf(this.options.trigger) + 1, contents.length);

                // Only query the server if we have a term and we haven't received a null response.
                // First check the current query to see if it already returned a null response.

                if(this.stopIndex == contents.lastIndexOf(this.options.trigger) && term.length > this.stopLength) { term = ''; }

                if(term.length > 0) {
                    // Updates the hidden field to check if a name was removed so that we can put them back in the list.
                    return this._search(term);
                else this.close();

        // Slightly altered the default ajax call to stop querying after the search produced no results.
        // This is to prevent unnecessary querying.

        _initSource: function() {
            var self = this, array, url;
            if ( $.isArray(this.options.source) ) {
                array = this.options.source;
                this.source = function( request, response ) {
                    response( $.ui.autocomplete.filter(array, request.term) );
            } else if ( typeof this.options.source === "string" ) {
                url = this.options.source;
                this.source = function( request, response ) {
                    if ( self.xhr ) {
                    self.xhr = $.ajax({
                        url: url,
                        data: request,
                        dataType: 'json',
                        success: function(data) {
                            if(data != null) {
                                response($.map(data, function(item) {
                                    if (typeof item === "string") {
                                        label = item;
                                    else {
                                        label = item.label;
                                    // If the item has already been selected don't re-include it.
                                    if(!self.id_map[label] || self.options.allowDuplicates) {
                                        return item
                                self.stopLength = -1;
                                self.stopIndex = -1;
                            else {
                                // No results, record length of string and stop querying unless the length decreases
                                self.stopLength = request.term.length;
                                self.stopIndex = self.contents.lastIndexOf(self.options.trigger);
            } else {
                this.source = this.options.source;

        destroy: function() {

        // Gets the position of the cursor in the input box.

        getCursor: function() {
            var i = this.element[0];

            if(i.selectionStart) {
                return i.selectionStart;
            else if(i.ownerDocument.selection) {
                var range = i.ownerDocument.selection.createRange();
                if(!range) return 0;
                var textrange = i.createTextRange();
                var textrange2 = textrange.duplicate();

                textrange2.setEndPoint('EndToStart', textrange);
                return textrange2.text.length;

        // Populates the hidden field with the contents of the entry box but with 
        // ID's instead of usernames.  Better for storage.

        updateHidden: function() {
            var trigger = this.options.trigger;
            var top = this.element.scrollTop();
            var contents = this.element.val();
            for(var key in this.id_map) {
                var find = trigger+key;
                find = find.replace(/[^a-zA-Z 0-9@]+/g,'\\$&');
                var regex = new RegExp(find, "g");
                var old_contents = contents;
                contents = contents.replace(regex, trigger+'['+this.id_map[key]+']');
                if(old_contents == contents) delete this.id_map[key];

})( jQuery, window , document );


Sign in to comment.
arthur_calvium   -  Oct 21, 2020


arthur_calvium   -  Oct 21, 2020

Great plugin! I was wondering if there's a way to display the label instead of the value in the text input? In jQuery autocomplete you can add option:

select: function (event, ui) {

Is there something like the above supported in this plugin? @Hawkee

atebsy   -  Apr 03, 2016
jiteshmg   -  Feb 11, 2016

Can I know how can I get the value of the label that is used inside the textarea, ie; if I use @chiicken , I need to get the value of that chicken 98765 when I submit the form.

adama36   -  Nov 04, 2015

Hey, How to modify this code to have multi-trigger (@#!) with many kind of data ?

Hawkee  -  Nov 04, 2015

That would require a fairly extensive rewrite. You may want to check into alternatives that offer that sort of functionality or you are more than welcome to add that functionality and do a pull request.

Sign in to comment

KadosTeam   -  Aug 21, 2015

Thanks @Hawkee !
I was trying to find exactly this autocomplete plugin and I was fighting with a lot of others plugin. Most of the time they don't work or they are nightmares to configure. And when they are OK, they don't work on a textarea displayed on a jquery dialog box :-(
Yours is perfect, light, very easy to use and working into a dialog box !
The only bug was the drop-down list can't be used with the up and down arrows but the hack of @UIdezigner solved that.(I'm using Jquery UI 1.11)

thangnn1510   -  Jul 24, 2015

The script doesn't work with jQuery 1.11.1. How do I fix it?


thangnn1510  -  Jul 24, 2015

nvm, I see it works, thanks.

Hawkee  -  Jul 24, 2015

Great! I didn't think it would work. Honestly, if I had this to do over I'd exclude jQuery UI and build the menus using the Bootstrap classes.

Sign in to comment

onlydreams   -  Apr 16, 2015

I try to use this in an angularJS modal page, but it doesn't work. Besides, I wrote a demo in a normal page, and it works well. Is there anything that I should pay attention to?

Hawkee  -  Apr 18, 2015

I'm sorry, but I have not tried this with AngularJS. You might want to check your console for any errors.

Sign in to comment

dannythebestguy   -  Jun 06, 2014

This does not work with TinyMCE. Is there a way around to get it working with TinyMCE

Maaruen   -  May 17, 2014

Hi, I am using this code in my app, and, then the second @, code don't work :(
In this case don't work neither.
any idea?

Hawkee  -  May 17, 2014

That certainly is strange. The versions match what I've got on jsfiddle, so my only thought is there is some encoding or whitespace issue. I noticed you're using a 1.7.2 theme rather than a 1.7.1 theme, but I don't think that should matter.

Sign in to comment

karo   -  May 10, 2014

great work!

please solve this bug and it will be even greater! ;)
it probably has to do with some regex somewhere, and it's not my strong side...

karo  -  May 10, 2014

hey there was supposed to be an image in my comment.
here it is, hosted:
it shows the "hidden" output, and the @[ id ] bug with @JavaScript, obviously because @Java is a substring of @JavaScript

Sign in to comment

charakalmast   -  Mar 03, 2014

How do i store a predefined json in a varaible as a source? I am getting a json in document.ready via ajax. I then store it in a variable and use it in the source, but this doesnt work.

charakalmast  -  Mar 03, 2014

and also how do i limit the number of results?

Hawkee  -  Mar 03, 2014

Make sure your json matches the format required for this to work. You can check the jsfiddle for a working example. As far as a limit, there is no functionality for that, but this might help:

charakalmast  -  Mar 03, 2014

The link worked for me.. I also figured out that i had to convert the string into a json object using JSON.parse(string) to make the variable work.
Can you also tell me how i can change the thing we search for? i.e. this current searches the label data, can i change it to something else

Hawkee  -  Mar 03, 2014

Why change it to something else?

charakalmast  -  Mar 04, 2014

actually i had implemented the whole autocomplete and used the label as something. i had changed a lot of code. now i want to add another searching factor which i will concat with some other value in the json.

Hawkee  -  Mar 04, 2014

You would have to modify the code to support multiple triggers.

adama36  -  Nov 04, 2015

How to modify it ?

Sign in to comment

HolyG   -  Jan 07, 2014

I have this working with an external source using the default @ trigger how might i adapt it So the trigger is either a “@” or “#” and to be able to pass the trigger into the source script to perform two different queries
also is there a way to set minLength:3 and a delay?

Hawkee  -  Jan 07, 2014

This doesn't support two triggers, but you can change the default trigger in the options. Refer to the option descriptions at the top of the code.

HolyG  -  Jan 08, 2014

OK If i have this in my HTML header

Is this Trigger overriding the Trigger in the script options?

I How do I pass the '#' into the source
Either like
source: "/cgi-bin/",
source: "/cgi-bin/",

$(function() {
hidden: '#hidden_inputbox',
source: "/cgi-bin/",
triger: "#"

Thank you

Hawkee  -  Jan 15, 2014

The trigger isn't passed along to the source. It just indicates that the text following it needs to be passed to the source.

Sign in to comment

charakalmast   -  Dec 03, 2013

How can i style the option list?

Hawkee  -  Dec 03, 2013

You'll have to use a different jQuery UI theme.

charakalmast  -  Dec 06, 2013

I tried a few different themes and found a good one, but i could not find what to manipulate in the css of the themes. I wanted to change the z-index of the option list, can you tell me what exactly i should change for that?

Hawkee  -  Dec 07, 2013

You probably want to inspect the element when it is open to find the class path. Then just add that to your custom CSS and adjust the z-index.

charakalmast  -  Dec 08, 2013

Hey. Thanks for the help. i got everything working fine now, but i have another question.
Is there a way i could change the color of the test that gets appended to the textarea?
The search also seems to be a bit slower than the ones i have used at other places. Any tips on how i can make it faster?

Hawkee  -  Dec 08, 2013

You'd have to adapt this to work for a contenteditable div to be able to change the colors. This would require some fairly complicated edits to the code. Are you using a server side search? The speed is dependent on your server. I like to use Sphinx for my server side searching as it's very fast.

charakalmast  -  Dec 09, 2013

thanks for all your help. will look into it.

Sign in to comment

charakalmast   -  Dec 03, 2013

Not working for me. I tried creating a simple version of this here: but it doesnt seem to work. What am i missing?

Hawkee  -  Dec 03, 2013

Try putting it into a $(document).ready function and see if that fixes it.

charakalmast  -  Dec 03, 2013

works now.. thanks!!

Sign in to comment

brewpoo   -  Oct 16, 2013

Is there anyway to position the drop down box in the general area of the cursor?

Hawkee  -  Oct 16, 2013

There was another project that branched from this on GitHub that does it. Follow the link above to the GitHub page.

wimyc  -  Nov 23, 2020

thank you

Sign in to comment

sloankev   -  Sep 29, 2013

@Hawkee this is working great! Thank you! I'm dealing with a large userbase and was just wondering how I could add a minLength attribute to only show results after typing 2 or 3 letters after the trigger character or only a certain number of results at a time. Any help would be appreciated.

Hawkee  -  Sep 30, 2013

You probably want to do this server-side. You may need to edit this slightly so self.stopLength has a minimum value. Otherwise the null result will stop the queries. You'll want it to continue querying until your min length is reached.

Sign in to comment

tushar_vikky   -  Jul 22, 2013

auto @mention user just like twitter has on its reply box. Is it possible?

Hawkee  -  Jul 22, 2013

Yes, you just pre-populate the hidden field with the userid in the format above and pre-populate the textarea with the visual representation of the mention.

tushar_vikky  -  Jul 22, 2013

I did that. But as soon as I select the textbox to type, it updates and copies the text to hidden_input.

Hawkee  -  Jul 22, 2013

Ah yes, because the id_map is not defined. You'll also need to pre-populate that as well.

tushar_vikky  -  Jul 22, 2013

It would help alot if you can point me to an example. And also how to define the id_map.

Hawkee  -  Jul 22, 2013

@tushar_vikky It is mentioned above. You'll just need to create a json encoded array with a single element representing the user you'd like pre-populated. If you need to see it in action try editing a comment you made here with a @mention and view the source for the id_map.

tushar_vikky  -  Jul 22, 2013

@Hawkee thanx a lot. It works perfect. :)

arimk  -  Feb 20, 2014

@Hawkee just to test the id_map as I have trouble using it
edit: got it to work now :)

Sign in to comment

despotbg   -  May 08, 2013

if you wont to search just by one word change those line
if (contents.indexOf(this.options.trigger) >= 0 && check_contents.match(regex)) ==>
if (contents.indexOf(this.options.trigger) >= 0 && check_contents.match(regex) && check_contents.indexOf(' ') <= 0)

despotbg   -  May 08, 2013

If you wont to search non latinic word change this line:
var regex = new RegExp('\B\'+this.options.trigger+'([\w\-]+)'); =>> var regex = new RegExp('\B\'+this.options.trigger+'([\S\-]+)');

despotbg   -  May 08, 2013

How to disable searching by multi words? I need just search by one word?

Hawkee  -  May 08, 2013

This is not something that can be disabled, but if your search returns no results after the user presses a space it will stop querying.

Sign in to comment

Piyush   -  Mar 21, 2013

After wasting a day on buggy jquery -input-mentions i found this working perfectly. Modified it for multi-trigger for my need. Here is quick hack with least possible change in existing code as others are also interested in it.
Add one more option at line 20
triggerArray: new Array('@','#'),

Add these in search function after assignment of variable at line 144 above

var triggerIndex = $.map(this.options.triggerArray,function(val,i){
return contents.lastIndexOf(val);
this.options.trigger = this.options.triggerArray[triggerIndex.indexOf(Math.max.apply(window,triggerIndex))];

Now request will be initiated at each element in triggerArray.

Hawkee  -  Mar 21, 2013

Great addition. How do you configure each dataset for each trigger?

Piyush  -  Mar 25, 2013

I think that won't be much of a problem. Passing an array of source on key value of trigger will do. Eg source: array('#'=>'/getHashTags' , '@'=>'getUserNames'). Then selecting the appropriate source in _initsource function.

Piyush  -  Mar 25, 2013

Hey i am facing an error when trying your this plugin with latest jquery ui and css bundle v 1.10.2. When traversing the items on autocomplete, error is throw at this line
if ( && /^previous/.test(direction) [line 127]
Uncaught TypeError: Object [object Object] has no method 'first'
Any fix for that ?

Hawkee  -  Mar 25, 2013

This hasn't been tested with jQuery UI 1.9 or 1.10 so there will likely be errors. It's currently only supported on 1.8.

Piyush  -  Mar 25, 2013

Perhaps that is because of use of some jquery internal function. You should probably consider it remaking it w/o touching internal functions. Anyways after commenting this section of code
if ( && /^previous/.test(direction) || && /^next/.test(direction) ) {;

everything seems to work fine. What exactly this part is doing ?
Currently i am adding one more feature to this plugin. Will update it once i am finished with it.

despotbg  -  May 07, 2013

Is there any chance to get multi-trigger soon? I need to have each dataset for each trigger :(

cgallegu  -  May 23, 2013

Add a "sources" property to the options in the following format: {"#" : 'url1', "@": 'url2'}.
Then modify your search function as following

var contents = this.element.val();
var cursorPos = this.getCursor();
var triggerIndex = $.map(this.options.triggerArray,function(val,i){
return contents.lastIndexOf(val);
this.options.trigger = this.options.triggerArray[triggerIndex.indexOf(Math.max.apply(window,triggerIndex))];
//Added for multi trigger datasource
if (this.options.sources !== undefined) {
this.options.source = this.options.sources[this.options.trigger];

UIdezigner  -  Jul 12, 2013

For the lines that Piyush commented out..... user should replaced with the following to work with jquery-ui 1.9.

if ( && /^previous/.test(direction) || && /^next/.test(direction) ) {
this._value( this.term );;

hooo wa !
this part allows navigation with arrow keys once menu in displayed.

Sign in to comment

Bicks   -  Mar 19, 2013

Everything works well, just wanted to know if we can remove the trigger after the autosuggessions has been selected like in facebook or twitter..

sud_mrjn   -  Feb 14, 2013

How does search.php works for this .. Can I have whole project demo as zip.. Thanks!!

Hawkee  -  Feb 14, 2013

There is a link to the GitHub project, but that only contains this core jQuery widget. You'll need to implement your own search.php or you can look at my node.js server side example.

sud_mrjn  -  Feb 15, 2013

My search.php is like this

//connection information
$host = "localhost";
$user = "root";
$password = "";
$database = "mention_list";
$param = $_GET["term"];

//make connection
$server = mysql_connect($host, $user, $password);
$connection = mysql_select_db($database, $server);

//query the database
$query = mysql_query("SELECT * FROM friends WHERE name REGEXP '^$param'");

//build array of results
for ($x = 0, $numrows = mysql_num_rows($query); $x < $numrows; $x++) {
    $row = mysql_fetch_assoc($query);

    $friends[$x] = array("name" => $row["name"]);       

//echo JSON to page

echo json_encode($friends);


But your widget also include trigger with search word eg. @cobol

Hawkee  -  Feb 17, 2013

Make sure you set the value and label fields before you do json_encode. You aren't creating your associative array correctly. It should be something like this:

$user['value'] = $row['user_id'];
$user['label'] = $row['name'];
$friends[] = $user;
Sign in to comment

cmer   -  Dec 29, 2012

Great work!

It does 95% of what I need, however. Does anybody know an alternative that acts more like Facebook, that is, doesn't require a trigger character (uses capital letters), allows spaces in the name (ie: first and last name) and highlights mentions directly in the textarea?

I've been using jquery.mentionsInput but it requires a trigger and doesn't support multi-word names.


Hawkee  -  Jan 03, 2013

This does allow spaces, but you would have to modify the code to accept capital letter triggers. This currently doesn't support contenteditable div's, so you won't get the highlighting. jQuery UI 1.9 now supports autocomplete on contenteditable divs, so it is possible but this will need to be adapted to work with version 1.9.

Sign in to comment

Chalien   -  Dec 29, 2012

@Hawkee WORKS PERFECT! Thanks man

Hawkee   -  Dec 29, 2012

@Chalien I believe you would have to change lines 145 and 146. You'll have to remove the character check from the regular expression and consider the parts that count the number of characters after the trigger.

Chalien   -  Dec 29, 2012

@Hawkee Yeah and in that way works perfect. but right now i need to used just with the @ and show all the user in the app. can you give me some advice about this?

Hawkee   -  Dec 28, 2012

@Chalien You need at least one letter otherwise there wouldn't be anything to search against.

Chalien   -  Dec 28, 2012

@Hawkee great plugin! I have a question. can i trigger the autocomplete just typing the @ or always i need to type a letter character after that. trying changing the this.triggerRegexp but nothing happened. THANKS!

Are you sure you want to unfollow this person?
Are you sure you want to delete this?
Click "Unsubscribe" to stop receiving notices pertaining to this post.
Click "Subscribe" to resume notices pertaining to this post.