diff --git a/docs/src/demos/Extensions/FloatingMenu/React/index.jsx b/docs/src/demos/Extensions/FloatingMenu/React/index.jsx
new file mode 100644
index 00000000..01787e1f
--- /dev/null
+++ b/docs/src/demos/Extensions/FloatingMenu/React/index.jsx
@@ -0,0 +1,43 @@
+import React from 'react'
+import { useEditor, EditorContent, BubbleMenu } from '@tiptap/react'
+import { defaultExtensions } from '@tiptap/starter-kit'
+import './styles.scss'
+
+export default () => {
+ const editor = useEditor({
+ extensions: [
+ ...defaultExtensions(),
+ ],
+ content: `
+
+ Hey, try to select some text here. There will popup a menu for selecting some inline styles. Remember: you have full control about content and styling of this menu.
+
+ `,
+ })
+
+ return (
+
+ {editor &&
+ editor.chain().focus().toggleBold().run()}
+ className={editor.isActive('bold') ? 'is-active' : ''}
+ >
+ bold
+
+ editor.chain().focus().toggleItalic().run()}
+ className={editor.isActive('italic') ? 'is-active' : ''}
+ >
+ italic
+
+ editor.chain().focus().toggleCode().run()}
+ className={editor.isActive('code') ? 'is-active' : ''}
+ >
+ code
+
+ }
+
+
+ )
+}
diff --git a/docs/src/demos/Extensions/FloatingMenu/React/styles.scss b/docs/src/demos/Extensions/FloatingMenu/React/styles.scss
new file mode 100644
index 00000000..12c87298
--- /dev/null
+++ b/docs/src/demos/Extensions/FloatingMenu/React/styles.scss
@@ -0,0 +1,5 @@
+.ProseMirror {
+ > * + * {
+ margin-top: 0.75em;
+ }
+}
diff --git a/docs/src/demos/Extensions/FloatingMenu/Vue/index.vue b/docs/src/demos/Extensions/FloatingMenu/Vue/index.vue
new file mode 100644
index 00000000..4954ea36
--- /dev/null
+++ b/docs/src/demos/Extensions/FloatingMenu/Vue/index.vue
@@ -0,0 +1,73 @@
+
+
+
+
+ h1
+
+
+ h2
+
+
+ bullet list
+
+
+ blockquote
+
+
+
+
+
+
+
+
+
diff --git a/packages/extension-floating-menu/README.md b/packages/extension-floating-menu/README.md
new file mode 100644
index 00000000..3b68aecc
--- /dev/null
+++ b/packages/extension-floating-menu/README.md
@@ -0,0 +1,14 @@
+# @tiptap/extension-floating-menu
+[](https://www.npmjs.com/package/@tiptap/extension-floating-menu)
+[](https://npmcharts.com/compare/tiptap?minimal=true)
+[](https://www.npmjs.com/package/@tiptap/extension-floating-menu)
+[](https://github.com/sponsors/ueberdosis)
+
+## Introduction
+tiptap is a headless wrapper around [ProseMirror](https://ProseMirror.net) – a toolkit for building rich text WYSIWYG editors, which is already in use at many well-known companies such as *New York Times*, *The Guardian* or *Atlassian*.
+
+## Offical Documentation
+Documentation can be found on the [tiptap website](https://tiptap.dev).
+
+## License
+tiptap is open-sourced software licensed under the [MIT license](https://github.com/ueberdosis/tiptap-next/blob/main/LICENSE.md).
diff --git a/packages/extension-floating-menu/package.json b/packages/extension-floating-menu/package.json
new file mode 100644
index 00000000..070f5463
--- /dev/null
+++ b/packages/extension-floating-menu/package.json
@@ -0,0 +1,31 @@
+{
+ "name": "@tiptap/extension-floating-menu",
+ "description": "floating-menu extension for tiptap",
+ "version": "2.0.0-beta.0",
+ "homepage": "https://tiptap.dev",
+ "keywords": [
+ "tiptap",
+ "tiptap extension"
+ ],
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/ueberdosis"
+ },
+ "main": "dist/tiptap-extension-floating-menu.cjs.js",
+ "umd": "dist/tiptap-extension-floating-menu.umd.js",
+ "module": "dist/tiptap-extension-floating-menu.esm.js",
+ "unpkg": "dist/tiptap-extension-floating-menu.bundle.umd.min.js",
+ "types": "dist/packages/extension-floating-menu/src/index.d.ts",
+ "files": [
+ "src",
+ "dist"
+ ],
+ "peerDependencies": {
+ "@tiptap/core": "^2.0.0-beta.1"
+ },
+ "dependencies": {
+ "prosemirror-state": "^1.3.4",
+ "prosemirror-view": "^1.18.2"
+ }
+}
diff --git a/packages/extension-floating-menu/src/floating-menu-plugin.ts b/packages/extension-floating-menu/src/floating-menu-plugin.ts
new file mode 100644
index 00000000..336c1d4b
--- /dev/null
+++ b/packages/extension-floating-menu/src/floating-menu-plugin.ts
@@ -0,0 +1,131 @@
+import { Editor } from '@tiptap/core'
+import { EditorState, Plugin, PluginKey } from 'prosemirror-state'
+import { EditorView } from 'prosemirror-view'
+
+export interface FloatingMenuPluginProps {
+ editor: Editor,
+ element: HTMLElement,
+}
+
+export type FloatingMenuViewProps = FloatingMenuPluginProps & {
+ view: EditorView,
+}
+
+export class FloatingMenuView {
+ public editor: Editor
+
+ public element: HTMLElement
+
+ public view: EditorView
+
+ public isActive = false
+
+ public top = 0
+
+ public preventHide = false
+
+ constructor({
+ editor,
+ element,
+ view,
+ }: FloatingMenuViewProps) {
+ this.editor = editor
+ this.element = element
+ this.view = view
+ this.element.addEventListener('mousedown', this.mousedownHandler, { capture: true })
+ this.editor.on('focus', this.focusHandler)
+ this.editor.on('blur', this.blurHandler)
+ this.render()
+ }
+
+ mousedownHandler = () => {
+ this.preventHide = true
+ }
+
+ focusHandler = () => {
+ // we use `setTimeout` to make sure `selection` is already updated
+ setTimeout(() => this.update(this.editor.view))
+ }
+
+ blurHandler = ({ event }: { event: FocusEvent }) => {
+ if (this.preventHide) {
+ this.preventHide = false
+
+ return
+ }
+
+ if (
+ event?.relatedTarget
+ && this.element.parentNode?.contains(event.relatedTarget as Node)
+ ) {
+ return
+ }
+
+ this.hide()
+ }
+
+ update(view: EditorView, oldState?: EditorState) {
+ const { state, composing } = view
+ const { doc, selection } = state
+ const isSame = oldState && oldState.doc.eq(doc) && oldState.selection.eq(selection)
+
+ if (composing || isSame) {
+ return
+ }
+
+ const { anchor, empty } = selection
+ const parent = this.element.offsetParent
+ const currentDom = view.domAtPos(anchor)
+ const currentElement = currentDom.node as Element
+ const isActive = currentElement.innerHTML === ' '
+ && currentElement.tagName === 'P'
+ && currentElement.parentNode === view.dom
+
+ if (!empty || !parent || !isActive) {
+ this.hide()
+
+ return
+ }
+
+ const parentBox = parent.getBoundingClientRect()
+ const cursorCoords = view.coordsAtPos(anchor)
+ const top = cursorCoords.top - parentBox.top
+
+ this.isActive = true
+ this.top = top
+
+ this.render()
+ }
+
+ render() {
+ Object.assign(this.element.style, {
+ position: 'absolute',
+ zIndex: 1,
+ visibility: this.isActive ? 'visible' : 'hidden',
+ opacity: this.isActive ? 1 : 0,
+ // left: `${this.left}px`,
+ top: `${this.top}px`,
+ // bottom: `${this.bottom}px`,
+ })
+ }
+
+ hide() {
+ this.isActive = false
+ this.render()
+ }
+
+ destroy() {
+ this.element.removeEventListener('mousedown', this.mousedownHandler)
+ this.editor.off('focus', this.focusHandler)
+ this.editor.off('blur', this.blurHandler)
+ }
+}
+
+export const FloatingMenuPluginKey = new PluginKey('menuFloating')
+
+export const FloatingMenuPlugin = (options: FloatingMenuPluginProps) => {
+ return new Plugin({
+ key: FloatingMenuPluginKey,
+ view: view => new FloatingMenuView({ view, ...options }),
+ })
+}
diff --git a/packages/extension-floating-menu/src/floating-menu.ts b/packages/extension-floating-menu/src/floating-menu.ts
new file mode 100644
index 00000000..8f4b1b12
--- /dev/null
+++ b/packages/extension-floating-menu/src/floating-menu.ts
@@ -0,0 +1,27 @@
+import { Extension } from '@tiptap/core'
+import { FloatingMenuPlugin, FloatingMenuPluginProps } from './floating-menu-plugin'
+
+export type FloatingMenuOptions = Omit & {
+ element: HTMLElement | null,
+}
+
+export const FloatingMenu = Extension.create({
+ name: 'bubbleMenu',
+
+ defaultOptions: {
+ element: null,
+ },
+
+ addProseMirrorPlugins() {
+ if (!this.options.element) {
+ return []
+ }
+
+ return [
+ FloatingMenuPlugin({
+ editor: this.editor,
+ element: this.options.element,
+ }),
+ ]
+ },
+})
diff --git a/packages/extension-floating-menu/src/index.ts b/packages/extension-floating-menu/src/index.ts
new file mode 100644
index 00000000..27bee69f
--- /dev/null
+++ b/packages/extension-floating-menu/src/index.ts
@@ -0,0 +1,6 @@
+import { FloatingMenu } from './floating-menu'
+
+export * from './floating-menu'
+export * from './floating-menu-plugin'
+
+export default FloatingMenu
diff --git a/packages/vue-2/src/FloatingMenu.ts b/packages/vue-2/src/FloatingMenu.ts
new file mode 100644
index 00000000..08f74d36
--- /dev/null
+++ b/packages/vue-2/src/FloatingMenu.ts
@@ -0,0 +1,39 @@
+import Vue, { PropType } from 'vue'
+import { FloatingMenuPlugin, FloatingMenuPluginKey, FloatingMenuPluginProps } from '@tiptap/extension-floating-menu'
+
+export const FloatingMenu = Vue.extend({
+ name: 'FloatingMenu',
+
+ props: {
+ editor: {
+ type: Object as PropType,
+ required: true,
+ },
+ },
+
+ watch: {
+ editor: {
+ immediate: true,
+ handler(editor: FloatingMenuPluginProps['editor']) {
+ if (!editor) {
+ return
+ }
+
+ this.$nextTick(() => {
+ editor.registerPlugin(FloatingMenuPlugin({
+ editor,
+ element: this.$el as HTMLElement,
+ }))
+ })
+ },
+ },
+ },
+
+ render(createElement) {
+ return createElement('div', {}, this.$slots.default)
+ },
+
+ beforeDestroy() {
+ this.editor.unregisterPlugin(FloatingMenuPluginKey)
+ },
+})
diff --git a/packages/vue-2/src/index.ts b/packages/vue-2/src/index.ts
index 4233fd98..f5b9136a 100644
--- a/packages/vue-2/src/index.ts
+++ b/packages/vue-2/src/index.ts
@@ -2,6 +2,7 @@ export * from '@tiptap/core'
export * from './BubbleMenu'
export { Editor } from './Editor'
export * from './EditorContent'
+export * from './FloatingMenu'
export * from './VueRenderer'
export * from './VueNodeViewRenderer'
export * from './NodeViewWrapper'