Contribute to this guide

guideChat with mentions

The mention feature allows developing rich–text applications (e.g. chats) with autocomplete suggestions displayed in a dedicated panel as the user types a pre–configured marker. For instance, in the editor below, type “@” to mention users and “#” to select from the list of available tags.

  • avatar
    Charles Flores @cflores 2 hours ago

    Thanks for another #yummy recipe, @dwilliams! Makes me #hungry just looking at the photos šŸ˜‹. Definitely adding it to my TODO list for our next #mediterranean potluck.

  • avatar
    Mildred Wilson @mwilson 4 hours ago

    Really appreciate the #vegetarian and #vegan variations of your recipes. So thoughtful of you! šŸŒ±

I agree with @mwilson šŸ‘. Itā€™s so nice of you to always be providing a few options to try! I love #greek cuisine with a modern twist, this one will be perfect to try.

Learn how to configure mention feeds in the dedicated guide and check out the full source code of this example if you want to implement your own chat using CKEditor 5 WYSIWYG editor.

# Source code

The following code will let you run the editor inside a chat application like in the example above. See the installing plugins guide to learn more.

Note: You may need to change the path to the assets/img both in the HTML and JavaScript code to load user avatars properly.

The HTML code of the application is listed below:

<div class="chat">
    <ul class="chat__posts">
        <li>
            <img src="../../assets/img/m_1.jpg" alt="avatar" class="photo">
            <div class="chat__posts__post__message">
                <strong>Charles Flores</strong>
                <a class="chat__posts__post__mailto-user" href="mailto:cflores@example.com">@cflores</a>
                <span class="chat__posts__post__time">2 hours ago</span>
                <p class="chat__posts__post__content">
                    Thanks for another <a class="mention" data-mention="#yummy" href="https://example.com/social/yummy">#yummy</a> recipe,
                    <a class="mention" data-mention="@dwilliams" href="mailto:dwilliams@example.com">@dwilliams</a>!
                    Makes me <a class="mention" data-mention="#hungry" href="https://example.com/social/hungry">#hungry</a>
                    just looking at the photos 😋. Definitely adding it to my TODO list for our next
                    <a class="mention" data-mention="#mediterranean" href="https://example.com/social/mediterranean">#mediterranean</a> potluck.
                </p>
            </div>
        </li>
        <li>
            <img src="../../assets/img/w_1.jpg" alt="avatar" class="photo">
            <div class="chat__posts__post__message">
                <strong>Mildred Wilson</strong>
                <a class="chat__posts__post__mailto-user" href="mailto:mwilson@example.com">@mwilson</a>
                <span class="chat__posts__post__time">4 hours ago</span>
                <p class="chat__posts__post__content">
                    Really appreciate the <a class="mention" data-mention="#vegetarian" href="https://example.com/social/vegetarian">#vegetarian</a>
                    and <a class="mention" data-mention="#vegan" href="https://example.com/social/vegan">#vegan</a> variations of your recipes.
                    So thoughtful of you! 🌱
                </p>
            </div>
        </li>
    </ul>
    <div class="chat__editor">
        <p>
            I agree with <a href="mailto:mwilson@example.com" class="mention" data-mention="@mwilson">@mwilson</a> 👍.
            It’s so nice of you to always be providing a few options to try! I love
            <a href="https://example.com/social/greek" class="mention" data-mention="#greek">#greek</a> cuisine with a modern twist,
            this one will be perfect to try.
        </p>
    </div>

    <button class="chat-send" type="button">Send</button>
</div>

<style>
    /* ---- General layout ---------------------------------------------------------------------- */

    .chat {
        margin-bottom: 1em
    }

    /* ---- Chat posts -------------------------------------------------------------------------- */

    .chat ul.chat__posts {
        border: 1px solid var(--ck-color-base-border);
        border-top-left-radius: var(--ck-border-radius);
        border-top-right-radius: var(--ck-border-radius);
        border-bottom: none;
        margin: 1em 0 0;
        padding: 1em;
        list-style-position: inside;
    }

    .chat ul.chat__posts li {
        display: flex;
    }

    .chat ul.chat__posts li.new-post {
        /* Highlight a new post in the chat. */
        animation: highlight 600ms ease-out;
    }

    .chat ul.chat__posts li + li {
        margin-top: 1em;
    }

    .chat .chat__posts li .photo {
        border-radius: 100%;
        height: 40px;
        margin-right: 1.5em;
    }

    .chat .chat__posts li .time {
        color: hsl(0, 0%, 72%);
        font-size: .9em;
    }

    .chat .chat__posts .chat__posts__post__message > strong::after,
    .chat__posts__post__mailto-user::after {
        content: "•";
        padding-left: 5px;
        padding-right: 5px;
        color: hsl(0, 0%, 72%);
    }

    @keyframes highlight {
        0% {
            background-color: yellow;
        }

        100% {
            background-color: white;
        }
    }

    /* ---- Chat editor ------------------------------------------------------------------------- */

    .chat .chat__editor {
        /* Anti–FOUC (flash of unstyled content). */
        padding: 1em;
        border: 1px solid var(--ck-color-base-border);
    }

    .chat .chat__editor + .ck.ck-editor {
        margin-top: 0;
    }

    .chat .chat__editor + .ck.ck-editor .ck.ck-toolbar {
        border-top-left-radius: 0;
        border-top-right-radius: 0;
    }

    .chat .chat__editor + .ck.ck-editor .ck-content.highlighted {
        animation: highlight 600ms ease-out;
    }

    /* ---- In–editor mention list --------------------------------------------------------------- */

    .ck-mentions .mention__item {
        display: block;
    }

    .ck-mentions .mention__item img {
        border-radius: 100%;
        height: 30px;
    }

    .ck-mentions .mention__item span {
        margin-left: .5em;
    }

    .ck-mentions .mention__item.ck-on span {
        color: var(--ck-color-base-background);
    }

    .ck-mentions .mention__item .mention__item__full-name {
        color: hsl(0, 0%, 45%);
    }

    .ck-mentions .mention__item:hover:not(.ck-on) .mention__item__full-name {
        color: hsl(0, 0%, 40%);
    }

    /* ---- Chat editor content styles ----------------------------------------------------------- */

    .chat .ck-content .mention {
        background: unset;
    }

    .chat .ck.ck-content a,
    .chat .chat__posts a {
        color: hsl(231, 89%, 53%);
    }
</style>

JavaScript code required to run the editor:

import ClassicEditor from '@ckeditor/ckeditor5-editor-classic/src/classiceditor';
import Essentials from '@ckeditor/ckeditor5-essentials/src/essentials';
import Bold from '@ckeditor/ckeditor5-basic-styles/src/bold';
import Italic from '@ckeditor/ckeditor5-basic-styles/src/italic';
import Underline from '@ckeditor/ckeditor5-basic-styles/src/underline';
import Strikethrough from '@ckeditor/ckeditor5-basic-styles/src/strikethrough';
import Mention from '@ckeditor/ckeditor5-mention/src/mention';
import Link from '@ckeditor/ckeditor5-link/src/link';
import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';

ClassicEditor
    .create( document.querySelector( '.chat__editor' ), {
        extraPlugins: [ Essentials, Paragraph, Mention, MentionLinks, Bold, Italic, Underline, Strikethrough, Link ],
        toolbar: {
            items: [
                'bold', 'italic', 'underline', 'strikethrough', '|', 'link', '|', 'undo', 'redo'
            ]
        },
        mention: {
            feeds: [
                {
                    marker: '@',
                    feed: [
                        { id: '@cflores', avatar: 'm_1', name: 'Charles Flores' },
                        { id: '@gjackson', avatar: 'm_2', name: 'Gerald Jackson' },
                        { id: '@wreed', avatar: 'm_3', name: 'Wayne Reed' },
                        { id: '@lgarcia', avatar: 'm_4', name: 'Louis Garcia' },
                        { id: '@rwilson', avatar: 'm_5', name: 'Roy Wilson' },
                        { id: '@mnelson', avatar: 'm_6', name: 'Matthew Nelson' },
                        { id: '@rwilliams', avatar: 'm_7', name: 'Randy Williams' },
                        { id: '@ajohnson', avatar: 'm_8', name: 'Albert Johnson' },
                        { id: '@sroberts', avatar: 'm_9', name: 'Steve Roberts' },
                        { id: '@kevans', avatar: 'm_10', name: 'Kevin Evans' },
                        { id: '@mwilson', avatar: 'w_1', name: 'Mildred Wilson' },
                        { id: '@mnelson', avatar: 'w_2', name: 'Melissa Nelson' },
                        { id: '@kallen', avatar: 'w_3', name: 'Kathleen Allen' },
                        { id: '@myoung', avatar: 'w_4', name: 'Mary Young' },
                        { id: '@arogers', avatar: 'w_5', name: 'Ashley Rogers' },
                        { id: '@dgriffin', avatar: 'w_6', name: 'Debra Griffin' },
                        { id: '@dwilliams', avatar: 'w_7', name: 'Denise Williams' },
                        { id: '@ajames', avatar: 'w_8', name: 'Amy James' },
                        { id: '@randerson', avatar: 'w_9', name: 'Ruby Anderson' },
                        { id: '@wlee', avatar: 'w_10', name: 'Wanda Lee' }
                    ],
                    itemRenderer: customItemRenderer
                },
                {
                    marker: '#',
                    feed: [
                        '#american', '#asian', '#baking', '#breakfast', '#cake', '#caribbean',
                        '#chinese', '#chocolate', '#cooking', '#dairy', '#delicious', '#delish',
                        '#dessert', '#desserts', '#dinner', '#eat', '#eating', '#eggs', '#fish',
                        '#food', '#foodgasm', '#foodie', '#foodporn', '#foods', '#french', '#fresh',
                        '#fusion', '#glutenfree', '#greek', '#grilling', '#halal', '#homemade',
                        '#hot', '#hungry', '#icecream', '#indian', '#italian', '#japanese', '#keto',
                        '#korean', '#lactosefree', '#lunch', '#meat', '#mediterranean', '#mexican',
                        '#moroccan', '#nom', '#nomnom', '#paleo', '#poultry', '#snack', '#spanish',
                        '#sugarfree', '#sweet', '#sweettooth', '#tasty', '#thai', '#vegan',
                        '#vegetarian', '#vietnamese', '#yum', '#yummy'
                    ]
                }
            ]
        }
    } )
    .then( editor => {
        const editingView = editor.editing.view;
        const rootElement = editingView.document.getRoot();

        window.editor = editor;

        // Clone the first message in the chat when "Send" is clicked, fill it with new data
        // and append to the chat list.
        document.querySelector( '.chat-send' ).addEventListener( 'click', () => {
            const message = editor.getData();

            if ( !message ) {
                editingView.change( writer => {
                    writer.addClass( 'highlighted', rootElement );
                    editingView.focus();
                } );

                setTimeout( () => {
                    editingView.change( writer => {
                        writer.removeClass( 'highlighted', rootElement );
                    } );
                }, 650 );

                return;
            }

            const clone = document.querySelector( '.chat__posts li' ).cloneNode( true );

            clone.classList.add( 'new-post' );
            clone.querySelector( 'img' ).src = '../../assets/img/m_0.jpg';
            clone.querySelector( 'strong' ).textContent = 'CKEditor User';

            const mailtoUser = clone.querySelector( '.chat__posts__post__mailto-user' );

            mailtoUser.textContent = '@ckeditor';
            mailtoUser.href = 'mailto:info@cksource.com';

            clone.querySelector( '.chat__posts__post__time' ).textContent = 'just now';
            clone.querySelector( '.chat__posts__post__content' ).innerHTML = message;

            document.querySelector( '.chat__posts' ).appendChild( clone );

            editor.setData( '' );
            editingView.focus();
        } );
    } )
    .catch( err => {
        console.error( err.stack );
    } );

/*
 * This plugin customizes the way mentions are handled in the editor model and data.
 * Instead of a classic <span class="mention"></span>,
 */
function MentionLinks( editor ) {
    // The upcast converter will convert a view
    //
    //		<a href="..." class="mention" data-mention="...">...</a>
    //
    // element to the model "mention" text attribute.
    editor.conversion.for( 'upcast' ).elementToAttribute( {
        view: {
            name: 'a',
            key: 'data-mention',
            classes: 'mention',
            attributes: {
                href: true
            }
        },
        model: {
            key: 'mention',
            value: viewItem => editor.plugins.get( 'Mention' ).toMentionAttribute( viewItem )
        },
        converterPriority: 'high'
    } );

    // Downcast the model "mention" text attribute to a view
    //
    //		<a href="..." class="mention" data-mention="...">...</a>
    //
    // element.
    editor.conversion.for( 'downcast' ).attributeToElement( {
        model: 'mention',
        view: ( modelAttributeValue, { writer } ) => {
            // Do not convert empty attributes (lack of value means no mention).
            if ( !modelAttributeValue ) {
                return;
            }

            let href;

            // User mentions are downcasted as mailto: links. Tags become normal URLs.
            if ( modelAttributeValue.id[ 0 ] === '@' ) {
                href = `mailto:${ modelAttributeValue.id.slice( 1 ) }@example.com`;
            } else {
                href = `https://example.com/social/${ modelAttributeValue.id.slice( 1 ) }`;
            }

            return writer.createAttributeElement( 'a', {
                class: 'mention',
                'data-mention': modelAttributeValue.id,
                href
            }, {
                // Make mention attribute to be wrapped by other attribute elements.
                priority: 20,
                // Prevent merging mentions together.
                id: modelAttributeValue.uid
            } );
        },
        converterPriority: 'high'
    } );
}

/*
 * Customizes the way the list of user suggestions is displayed.
 * Each user has an @id, a name and an avatar.
 */
function customItemRenderer( item ) {
    const itemElement = document.createElement( 'span' );
    const avatar = document.createElement( 'img' );
    const userNameElement = document.createElement( 'span' );
    const fullNameElement = document.createElement( 'span' );

    itemElement.classList.add( 'mention__item' );

    avatar.src = `../../assets/img/${ item.avatar }.jpg`;

    userNameElement.classList.add( 'mention__item__user-name' );
    userNameElement.textContent = item.id;

    fullNameElement.classList.add( 'mention__item__full-name' );
    fullNameElement.textContent = item.name;

    itemElement.appendChild( avatar );
    itemElement.appendChild( userNameElement );
    itemElement.appendChild( fullNameElement );

    return itemElement;
}