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-dompnpm add @emojisense/lexical emojisense lexical @lexical/react react react-domyarn add @emojisense/lexical emojisense lexical @lexical/react react react-dombun add @emojisense/lexical emojisense lexical @lexical/react react react-domSetup
With @emojisense/react, which loads the packs and builds the engine:
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
| Prop | Default | Notes |
|---|---|---|
engine | — | The engine, or undefined |
semantic | — | A semantic provider, for example createSemanticClient or chainProviders(shards, api) |
locale | first pack | Preferred locale for ranking and labels |
limit | 8 | Menu size |
debounceMs | 200 | Delay 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 |
minQueryLength | 1 | Characters after the trigger before the menu opens |
shortcodes | true | Replace a typed :name: when name is an exact shortcode or emoji name |
ariaLabel | "Emoji suggestions" | Accessible name of the default menu |
menuRenderFn | default menu | Lexical’s MenuRenderFn<EmojiOption>, called only while there are results |
anchorClassName | — | Class for the element Lexical positions at the caret |
commandPriority | COMMAND_PRIORITY_CRITICAL | Priority of the open menu’s key handlers |
menuContainer | document.body | The 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.
Mount the menu in your own frame
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>.
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
<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.