diff --git a/demos/src/Nodes/Mention/React/MentionList.jsx b/demos/src/Nodes/Mention/React/MentionList.jsx
new file mode 100644
index 00000000..0091b63e
--- /dev/null
+++ b/demos/src/Nodes/Mention/React/MentionList.jsx
@@ -0,0 +1,65 @@
+import React, {
+ useState, useEffect, forwardRef, useImperativeHandle,
+} from 'react'
+import './MentionList.scss'
+
+export default forwardRef((props, ref) => {
+ const [selectedIndex, setSelectedIndex] = useState(0)
+
+ const selectItem = index => {
+ const item = props.items[index]
+
+ if (item) {
+ props.command({ id: item })
+ }
+ }
+
+ const upHandler = () => {
+ setSelectedIndex((selectedIndex + props.items.length - 1) % props.items.length)
+ }
+
+ const downHandler = () => {
+ setSelectedIndex((selectedIndex + 1) % props.items.length)
+ }
+
+ const enterHandler = () => {
+ selectItem(selectedIndex)
+ }
+
+ useEffect(() => setSelectedIndex(0), [props.items])
+
+ useImperativeHandle(ref, () => ({
+ onKeyDown: ({ event }) => {
+ if (event.key === 'ArrowUp') {
+ upHandler()
+ return true
+ }
+
+ if (event.key === 'ArrowDown') {
+ downHandler()
+ return true
+ }
+
+ if (event.key === 'Enter') {
+ enterHandler()
+ return true
+ }
+
+ return false
+ },
+ }))
+
+ return (
+
+ {props.items.map((item, index) => (
+
+ ))}
+
+ )
+})
diff --git a/demos/src/Nodes/Mention/React/MentionList.scss b/demos/src/Nodes/Mention/React/MentionList.scss
new file mode 100644
index 00000000..89e81cd2
--- /dev/null
+++ b/demos/src/Nodes/Mention/React/MentionList.scss
@@ -0,0 +1,25 @@
+.items {
+ background: #fff;
+ border-radius: 0.5rem;
+ box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.05), 0px 10px 20px rgba(0, 0, 0, 0.1);
+ color: rgba(0, 0, 0, 0.8);
+ font-size: 0.9rem;
+ overflow: hidden;
+ padding: 0.2rem;
+ position: relative;
+}
+
+.item {
+ background: transparent;
+ border: 1px solid transparent;
+ border-radius: 0.4rem;
+ display: block;
+ margin: 0;
+ padding: 0.2rem 0.4rem;
+ text-align: left;
+ width: 100%;
+
+ &.is-selected {
+ border-color: #000;
+ }
+}
diff --git a/demos/src/Nodes/Mention/React/index.html b/demos/src/Nodes/Mention/React/index.html
new file mode 100644
index 00000000..65a1893d
--- /dev/null
+++ b/demos/src/Nodes/Mention/React/index.html
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/demos/src/Nodes/Mention/React/index.jsx b/demos/src/Nodes/Mention/React/index.jsx
new file mode 100644
index 00000000..38c2bef9
--- /dev/null
+++ b/demos/src/Nodes/Mention/React/index.jsx
@@ -0,0 +1,37 @@
+import React from 'react'
+import { useEditor, EditorContent } from '@tiptap/react'
+import Document from '@tiptap/extension-document'
+import Paragraph from '@tiptap/extension-paragraph'
+import Text from '@tiptap/extension-text'
+import Mention from '@tiptap/extension-mention'
+import suggestion from './suggestion'
+import './styles.scss'
+
+export default () => {
+ const editor = useEditor({
+ extensions: [
+ Document,
+ Paragraph,
+ Text,
+ Mention.configure({
+ HTMLAttributes: {
+ class: 'mention',
+ },
+ suggestion,
+ }),
+ ],
+ content: `
+ Hi everyone! Don’t forget the daily stand up at 8 AM.
+ Would you mind to share what you’ve been working on lately? We fear not much happened since Dirty Dancing.
+
Let’s go through your most important points quickly.
+ I have a meeting with and don’t want to come late.
+ – Thanks, your big boss
+ `,
+ })
+
+ if (!editor) {
+ return null
+ }
+
+ return
+}
diff --git a/demos/src/Nodes/Mention/React/index.spec.js b/demos/src/Nodes/Mention/React/index.spec.js
new file mode 100644
index 00000000..45be4bfd
--- /dev/null
+++ b/demos/src/Nodes/Mention/React/index.spec.js
@@ -0,0 +1,7 @@
+context('/src/Nodes/Mention/React/', () => {
+ before(() => {
+ cy.visit('/src/Nodes/Mention/React/')
+ })
+
+ // TODO: Write tests
+})
diff --git a/demos/src/Nodes/Mention/React/styles.scss b/demos/src/Nodes/Mention/React/styles.scss
new file mode 100644
index 00000000..596151fc
--- /dev/null
+++ b/demos/src/Nodes/Mention/React/styles.scss
@@ -0,0 +1,12 @@
+.ProseMirror {
+ > * + * {
+ margin-top: 0.75em;
+ }
+}
+
+.mention {
+ border: 1px solid #000;
+ border-radius: 0.4rem;
+ box-decoration-break: clone;
+ padding: 0.1rem 0.3rem;
+}
diff --git a/demos/src/Nodes/Mention/React/suggestion.js b/demos/src/Nodes/Mention/React/suggestion.js
new file mode 100644
index 00000000..911addee
--- /dev/null
+++ b/demos/src/Nodes/Mention/React/suggestion.js
@@ -0,0 +1,84 @@
+import { ReactRenderer } from '@tiptap/react'
+import tippy from 'tippy.js'
+import MentionList from './MentionList.jsx'
+
+export default {
+ items: ({ query }) => {
+ return [
+ 'Lea Thompson',
+ 'Cyndi Lauper',
+ 'Tom Cruise',
+ 'Madonna',
+ 'Jerry Hall',
+ 'Joan Collins',
+ 'Winona Ryder',
+ 'Christina Applegate',
+ 'Alyssa Milano',
+ 'Molly Ringwald',
+ 'Ally Sheedy',
+ 'Debbie Harry',
+ 'Olivia Newton-John',
+ 'Elton John',
+ 'Michael J. Fox',
+ 'Axl Rose',
+ 'Emilio Estevez',
+ 'Ralph Macchio',
+ 'Rob Lowe',
+ 'Jennifer Grey',
+ 'Mickey Rourke',
+ 'John Cusack',
+ 'Matthew Broderick',
+ 'Justine Bateman',
+ 'Lisa Bonet',
+ ]
+ .filter(item => item.toLowerCase().startsWith(query.toLowerCase()))
+ .slice(0, 5)
+ },
+
+ render: () => {
+ let component
+ let popup
+
+ return {
+ onStart: props => {
+ component = new ReactRenderer(MentionList, {
+ props,
+ editor: props.editor,
+ })
+
+ popup = tippy('body', {
+ getReferenceClientRect: props.clientRect,
+ appendTo: () => document.body,
+ content: component.element,
+ showOnCreate: true,
+ interactive: true,
+ trigger: 'manual',
+ placement: 'bottom-start',
+ })
+ },
+
+ onUpdate(props) {
+ component.updateProps(props)
+
+ popup[0].setProps({
+ getReferenceClientRect: props.clientRect,
+ })
+ },
+
+ onKeyDown(props) {
+ if (props.event.key === 'Escape') {
+ popup[0].hide()
+
+ return true
+ }
+
+ return component.ref?.onKeyDown(props)
+ },
+
+ onExit() {
+ popup[0].destroy()
+ component.destroy()
+ },
+ }
+ },
+}