Skip to article
Integrations / Lexical

Lexical

Colon autocomplete for Lexical editors in React.

On this page

@emojisense/lexical adds : emoji autocomplete to Lexical editors in React. Type :jurassic and the menu shows 🦖 🦕 🦟. Type :fire: and it becomes 🔥. It is built on Lexical’s own LexicalTypeaheadMenuPlugin.

Install

npm install @emojisense/lexical emojisense lexical @lexical/react react react-dom

Setup

With @emojisense/react, which loads the packs and builds the engine:

Editor.tsx
import { EmojiAutocompletePlugin } from "@emojisense/lexical";
import "@emojisense/lexical/styles.css"; // optional default look
import { useEmojisense } from "@emojisense/react";
import { LexicalComposer } from "@lexical/react/LexicalComposer";
import { ContentEditable } from "@lexical/react/LexicalContentEditable";
import { LexicalErrorBoundary } from "@lexical/react/LexicalErrorBoundary";
import { RichTextPlugin } from "@lexical/react/LexicalRichTextPlugin";

export function Editor() {
  const sense = useEmojisense({
    packBaseUrl: "https://api.emojisense.com/v1/pack/0.1.0",
    endpoint: "https://api.emojisense.com",
  });

  return (
    <LexicalComposer initialConfig={{ namespace: "chat", onError: console.error }}>
      <RichTextPlugin
        contentEditable={<ContentEditable />}
        ErrorBoundary={LexicalErrorBoundary}
      />
      <EmojiAutocompletePlugin
        engine={sense.engine}
        semantic={sense.semantic}
        skinTone="medium"
      />
    </LexicalComposer>
  );
}

Without the React hooks, pass engine={createEngine(await loadPacks({ baseUrl }))} from emojisense. While engine is undefined, because the packs are still loading, the plugin stays inactive.

Props

PropDefaultNotes
engine—The engine, or undefined
semantic—A semantic provider, for example createSemanticClient or chainProviders(shards, api)
localefirst packPreferred locale for ranking and labels
limit8Menu size
debounceMs200Delay before a semantic request
skinTone"none"Applied to the shown and inserted emoji
trigger":"The menu opens after a space, a ( or a line start
minQueryLength1Characters after the trigger before the menu opens
shortcodestrueReplace a typed :name: when name is an exact shortcode or emoji name
ariaLabel"Emoji suggestions"Accessible name of the default menu
menuRenderFndefault menuLexical’s MenuRenderFn<EmojiOption>, called only while there are results
anchorClassName—Class for the element Lexical positions at the caret
commandPriorityCOMMAND_PRIORITY_CRITICALPriority of the open menu’s key handlers
menuContainerdocument.bodyThe element the menu mounts into. See below.

Keyboard

↑ and ↓ move the active option. Enter or Tab inserts it in place of :query, and Shift+Enter is left to the editor. Escape closes the menu and keeps the typed text until the next word. :trex:, :+1: and :sweat_smile: turn into emoji while you type.

While the menu is open, it gets Enter, Tab, ↑, ↓ and Escape first, because its handlers use COMMAND_PRIORITY_CRITICAL. So TablePlugin and code blocks do not take Tab from it. While the menu is closed, all keys go to the editor. The menu never scrolls the page.

By default the menu mounts on <body>. To keep it inside a dialog or a scroll panel, pass that element as menuContainer. Keep it in state with a callback ref, so the plugin gets it after the first render. While it is null, the menu mounts on <body>.

TSX
function Editor() {
  const [frame, setFrame] = useState<HTMLDivElement | null>(null);
  return (
    <div ref={setFrame} style={{ position: "relative" }}>
      <LexicalComposer initialConfig={{ namespace: "demo", onError: console.error }}>
        <RichTextPlugin
          contentEditable={<ContentEditable />}
          ErrorBoundary={LexicalErrorBoundary}
        />
        <EmojiAutocompletePlugin engine={engine} menuContainer={frame} />
      </LexicalComposer>
    </div>
  );
}

Your own menu

TSX
<EmojiAutocompletePlugin
  engine={engine}
  menuRenderFn={(anchor, { options, selectedIndex, selectOptionAndCleanUp }) =>
    anchor.current &&
    createPortal(<MyMenu options={options} /* … */ />, anchor.current)
  }
/>

Each EmojiOption has suggestion: { emoji, id, label, source }, with the skin tone applied. Give option rows the ids typeahead-item-<index>, so the editor’s aria-activedescendant resolves. The default EmojiMenu is exported for reuse, and registerShortcodeTransform(editor, resolve) gives :name: completion without a menu.