diff --git a/resources/js/services/events.ts b/resources/js/services/events.ts
index 32c70f5a8..be9fba7ec 100644
--- a/resources/js/services/events.ts
+++ b/resources/js/services/events.ts
@@ -7,7 +7,7 @@ export class EventManager {
     /**
      * Emit a custom event for any handlers to pick-up.
      */
-    emit(eventName: string, eventData: {}): void {
+    emit(eventName: string, eventData: {} = {}): void {
         this.stack.push({name: eventName, data: eventData});
 
         const listenersToRun = this.listeners[eventName] || [];
diff --git a/resources/js/wysiwyg/index.ts b/resources/js/wysiwyg/index.ts
index a07fbd789..0a939baf4 100644
--- a/resources/js/wysiwyg/index.ts
+++ b/resources/js/wysiwyg/index.ts
@@ -12,6 +12,7 @@ import {handleDropEvents} from "./services/drop-handling";
 import {registerTaskListHandler} from "./ui/framework/helpers/task-list-handler";
 import {registerTableSelectionHandler} from "./ui/framework/helpers/table-selection-handler";
 import {el} from "./utils/dom";
+import {registerShortcuts} from "./services/shortcuts";
 
 export function createPageEditorInstance(container: HTMLElement, htmlContent: string, options: Record<string, any> = {}): SimpleWysiwygEditorInterface {
     const config: CreateEditorArgs = {
@@ -48,6 +49,7 @@ export function createPageEditorInstance(container: HTMLElement, htmlContent: st
     mergeRegister(
         registerRichText(editor),
         registerHistory(editor, createEmptyHistoryState(), 300),
+        registerShortcuts(editor),
         registerTableResizer(editor, editWrap),
         registerTableSelectionHandler(editor),
         registerTaskListHandler(editor, editArea),
diff --git a/resources/js/wysiwyg/services/shortcuts.ts b/resources/js/wysiwyg/services/shortcuts.ts
new file mode 100644
index 000000000..235c2788a
--- /dev/null
+++ b/resources/js/wysiwyg/services/shortcuts.ts
@@ -0,0 +1,91 @@
+import {COMMAND_PRIORITY_HIGH, FORMAT_TEXT_COMMAND, KEY_ENTER_COMMAND, LexicalEditor} from "lexical";
+import {
+    cycleSelectionCalloutFormats,
+    formatCodeBlock,
+    toggleSelectionAsBlockquote,
+    toggleSelectionAsHeading,
+    toggleSelectionAsParagraph
+} from "../utils/formats";
+import {HeadingTagType} from "@lexical/rich-text";
+
+function headerHandler(editor: LexicalEditor, tag: HeadingTagType): boolean {
+    toggleSelectionAsHeading(editor, tag);
+    return true;
+}
+
+function wrapFormatAction(formatAction: (editor: LexicalEditor) => any): ShortcutAction {
+    return (editor: LexicalEditor) => {
+        formatAction(editor);
+        return true;
+    };
+}
+
+function toggleInlineCode(editor: LexicalEditor): boolean {
+    editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'code');
+    return true;
+}
+
+type ShortcutAction = (editor: LexicalEditor) => boolean;
+
+const actionsByKeys: Record<string, ShortcutAction> = {
+    // Save draft
+    'ctrl+s': () => {
+        window.$events.emit('editor-save-draft');
+        return true;
+    },
+    'ctrl+enter': () => {
+        window.$events.emit('editor-save-page');
+        return true;
+    },
+    'ctrl+1': (editor) => headerHandler(editor, 'h1'),
+    'ctrl+2': (editor) => headerHandler(editor, 'h2'),
+    'ctrl+3': (editor) => headerHandler(editor, 'h3'),
+    'ctrl+4': (editor) => headerHandler(editor, 'h4'),
+    'ctrl+5': wrapFormatAction(toggleSelectionAsParagraph),
+    'ctrl+d': wrapFormatAction(toggleSelectionAsParagraph),
+    'ctrl+6': wrapFormatAction(toggleSelectionAsBlockquote),
+    'ctrl+q': wrapFormatAction(toggleSelectionAsBlockquote),
+    'ctrl+7': wrapFormatAction(formatCodeBlock),
+    'ctrl+e': wrapFormatAction(formatCodeBlock),
+    'ctrl+8': toggleInlineCode,
+    'ctrl+shift+e': toggleInlineCode,
+    'ctrl+9': wrapFormatAction(cycleSelectionCalloutFormats),
+
+    // TODO Lists
+    // TODO Links
+    // TODO Link selector
+};
+
+function createKeyDownListener(editor: LexicalEditor): (e: KeyboardEvent) => void {
+    return (event: KeyboardEvent) => {
+        // TODO - Mac Cmd support
+        const combo = `${event.ctrlKey ? 'ctrl+' : ''}${event.shiftKey ? 'shift+' : ''}${event.key}`.toLowerCase();
+        console.log(`pressed: ${combo}`);
+        if (actionsByKeys[combo]) {
+            const handled = actionsByKeys[combo](editor);
+            if (handled) {
+                event.stopPropagation();
+                event.preventDefault();
+            }
+        }
+    };
+}
+
+function overrideDefaultCommands(editor: LexicalEditor) {
+    // Prevent default ctrl+enter command
+    editor.registerCommand(KEY_ENTER_COMMAND, (event) => {
+        return event?.ctrlKey ? true : false
+    }, COMMAND_PRIORITY_HIGH);
+}
+
+export function registerShortcuts(editor: LexicalEditor) {
+    const listener = createKeyDownListener(editor);
+    overrideDefaultCommands(editor);
+
+    return editor.registerRootListener((rootElement: null | HTMLElement, prevRootElement: null | HTMLElement) => {
+        // add the listener to the current root element
+        rootElement?.addEventListener('keydown', listener);
+        // remove the listener from the old root element
+        prevRootElement?.removeEventListener('keydown', listener);
+    });
+}
\ No newline at end of file
diff --git a/resources/js/wysiwyg/todo.md b/resources/js/wysiwyg/todo.md
index fec38271a..75263c927 100644
--- a/resources/js/wysiwyg/todo.md
+++ b/resources/js/wysiwyg/todo.md
@@ -2,14 +2,13 @@
 
 ## In progress
 
-//
+- Keyboard shortcuts support
 
 ## Main Todo
 
 
 - Alignments: Handle inline block content (image, video)
 - Image paste upload
-- Keyboard shortcuts support
 - Support media src conversions (https://github.com/tinymce/tinymce/blob/release/6.6/modules/tinymce/src/plugins/media/main/ts/core/UrlPatterns.ts)
 - Media resize support (like images)
 - Table caption text support
diff --git a/resources/js/wysiwyg/ui/defaults/buttons/block-formats.ts b/resources/js/wysiwyg/ui/defaults/buttons/block-formats.ts
index eba903263..80e493486 100644
--- a/resources/js/wysiwyg/ui/defaults/buttons/block-formats.ts
+++ b/resources/js/wysiwyg/ui/defaults/buttons/block-formats.ts
@@ -1,16 +1,19 @@
 import {$createCalloutNode, $isCalloutNodeOfCategory, CalloutCategory} from "../../../nodes/callout";
 import {EditorButtonDefinition} from "../../framework/buttons";
 import {EditorUiContext} from "../../framework/core";
-import {$createParagraphNode, $isParagraphNode, BaseSelection, LexicalNode} from "lexical";
+import {$isParagraphNode, BaseSelection, LexicalNode} from "lexical";
 import {
-    $createHeadingNode,
-    $createQuoteNode,
     $isHeadingNode,
     $isQuoteNode,
     HeadingNode,
     HeadingTagType
 } from "@lexical/rich-text";
 import {$selectionContainsNodeType, $toggleSelectionBlockNodeType} from "../../../utils/selection";
+import {
+    toggleSelectionAsBlockquote,
+    toggleSelectionAsHeading,
+    toggleSelectionAsParagraph
+} from "../../../utils/formats";
 
 function buildCalloutButton(category: CalloutCategory, name: string): EditorButtonDefinition {
     return {
@@ -42,12 +45,7 @@ function buildHeaderButton(tag: HeadingTagType, name: string): EditorButtonDefin
     return {
         label: name,
         action(context: EditorUiContext) {
-            context.editor.update(() => {
-                $toggleSelectionBlockNodeType(
-                    (node) => isHeaderNodeOfTag(node, tag),
-                    () => $createHeadingNode(tag),
-                )
-            });
+            toggleSelectionAsHeading(context.editor, tag);
         },
         isActive(selection: BaseSelection|null): boolean {
             return $selectionContainsNodeType(selection, (node) => isHeaderNodeOfTag(node, tag));
@@ -63,9 +61,7 @@ export const h5: EditorButtonDefinition = buildHeaderButton('h5', 'Tiny Header')
 export const blockquote: EditorButtonDefinition = {
     label: 'Blockquote',
     action(context: EditorUiContext) {
-        context.editor.update(() => {
-            $toggleSelectionBlockNodeType($isQuoteNode, $createQuoteNode);
-        });
+        toggleSelectionAsBlockquote(context.editor);
     },
     isActive(selection: BaseSelection|null): boolean {
         return $selectionContainsNodeType(selection, $isQuoteNode);
@@ -75,9 +71,7 @@ export const blockquote: EditorButtonDefinition = {
 export const paragraph: EditorButtonDefinition = {
     label: 'Paragraph',
     action(context: EditorUiContext) {
-        context.editor.update(() => {
-            $toggleSelectionBlockNodeType($isParagraphNode, $createParagraphNode);
-        });
+        toggleSelectionAsParagraph(context.editor);
     },
     isActive(selection: BaseSelection|null): boolean {
         return $selectionContainsNodeType(selection, $isParagraphNode);
diff --git a/resources/js/wysiwyg/ui/defaults/buttons/objects.ts b/resources/js/wysiwyg/ui/defaults/buttons/objects.ts
index 96a92ff22..3494096a2 100644
--- a/resources/js/wysiwyg/ui/defaults/buttons/objects.ts
+++ b/resources/js/wysiwyg/ui/defaults/buttons/objects.ts
@@ -28,11 +28,12 @@ import {$isMediaNode, MediaNode} from "../../../nodes/media";
 import {
     $getNodeFromSelection,
     $insertNewBlockNodeAtSelection,
-    $selectionContainsNodeType
+    $selectionContainsNodeType, getLastSelection
 } from "../../../utils/selection";
 import {$isDiagramNode, $openDrawingEditorForNode, showDiagramManagerForInsert} from "../../../utils/diagrams";
 import {$createLinkedImageNodeFromImageData, showImageManager} from "../../../utils/images";
 import {$showImageForm} from "../forms/objects";
+import {formatCodeBlock} from "../../../utils/formats";
 
 export const link: EditorButtonDefinition = {
     label: 'Insert/edit link',
@@ -72,7 +73,7 @@ export const unlink: EditorButtonDefinition = {
     icon: unlinkIcon,
     action(context: EditorUiContext) {
         context.editor.update(() => {
-            const selection = context.lastSelection;
+            const selection = getLastSelection(context.editor);
             const selectedLink = $getNodeFromSelection(selection, $isLinkNode) as LinkNode | null;
             const selectionPoints = selection?.getStartEndPoints();
 
@@ -98,7 +99,8 @@ export const image: EditorButtonDefinition = {
     icon: imageIcon,
     action(context: EditorUiContext) {
         context.editor.getEditorState().read(() => {
-            const selectedImage = $getNodeFromSelection(context.lastSelection, $isImageNode) as ImageNode | null;
+            const selection = getLastSelection(context.editor);
+            const selectedImage = $getNodeFromSelection(selection, $isImageNode) as ImageNode | null;
             if (selectedImage) {
                 $showImageForm(selectedImage, context);
                 return;
@@ -134,21 +136,7 @@ export const codeBlock: EditorButtonDefinition = {
     label: 'Insert code block',
     icon: codeBlockIcon,
     action(context: EditorUiContext) {
-        context.editor.getEditorState().read(() => {
-            const selection = $getSelection();
-            const codeBlock = $getNodeFromSelection(context.lastSelection, $isCodeBlockNode) as (CodeBlockNode | null);
-            if (codeBlock === null) {
-                context.editor.update(() => {
-                    const codeBlock = $createCodeBlockNode();
-                    codeBlock.setCode(selection?.getTextContent() || '');
-                    $insertNewBlockNodeAtSelection(codeBlock, true);
-                    $openCodeEditorForNode(context.editor, codeBlock);
-                    codeBlock.selectStart();
-                });
-            } else {
-                $openCodeEditorForNode(context.editor, codeBlock);
-            }
-        });
+        formatCodeBlock(context.editor);
     },
     isActive(selection: BaseSelection | null): boolean {
         return $selectionContainsNodeType(selection, $isCodeBlockNode);
@@ -165,8 +153,8 @@ export const diagram: EditorButtonDefinition = {
     icon: diagramIcon,
     action(context: EditorUiContext) {
         context.editor.getEditorState().read(() => {
-            const selection = $getSelection();
-            const diagramNode = $getNodeFromSelection(context.lastSelection, $isDiagramNode) as (DiagramNode | null);
+            const selection = getLastSelection(context.editor);
+            const diagramNode = $getNodeFromSelection(selection, $isDiagramNode) as (DiagramNode | null);
             if (diagramNode === null) {
                 context.editor.update(() => {
                     const diagram = $createDiagramNode();
diff --git a/resources/js/wysiwyg/ui/defaults/forms/objects.ts b/resources/js/wysiwyg/ui/defaults/forms/objects.ts
index 2ad27f749..2aefe5414 100644
--- a/resources/js/wysiwyg/ui/defaults/forms/objects.ts
+++ b/resources/js/wysiwyg/ui/defaults/forms/objects.ts
@@ -10,7 +10,7 @@ import {$isImageNode, ImageNode} from "../../../nodes/image";
 import {$createLinkNode, $isLinkNode} from "@lexical/link";
 import {$createMediaNodeFromHtml, $createMediaNodeFromSrc, $isMediaNode, MediaNode} from "../../../nodes/media";
 import {$insertNodeToNearestRoot} from "@lexical/utils";
-import {$getNodeFromSelection} from "../../../utils/selection";
+import {$getNodeFromSelection, getLastSelection} from "../../../utils/selection";
 import {EditorFormModal} from "../../framework/modals";
 import {EditorActionField} from "../../framework/blocks/action-field";
 import {EditorButton} from "../../framework/buttons";
@@ -39,7 +39,8 @@ export const image: EditorFormDefinition = {
     submitText: 'Apply',
     async action(formData, context: EditorUiContext) {
         context.editor.update(() => {
-            const selectedImage = $getNodeFromSelection(context.lastSelection, $isImageNode);
+            const selection = getLastSelection(context.editor);
+            const selectedImage = $getNodeFromSelection(selection, $isImageNode);
             if ($isImageNode(selectedImage)) {
                 selectedImage.setSrc(formData.get('src')?.toString() || '');
                 selectedImage.setAltText(formData.get('alt')?.toString() || '');
diff --git a/resources/js/wysiwyg/ui/framework/core.ts b/resources/js/wysiwyg/ui/framework/core.ts
index 3e9f1e3d9..b6fe52dcd 100644
--- a/resources/js/wysiwyg/ui/framework/core.ts
+++ b/resources/js/wysiwyg/ui/framework/core.ts
@@ -15,7 +15,6 @@ export type EditorUiContext = {
     scrollDOM: HTMLElement; // DOM element which is the main content scroll container
     translate: (text: string) => string; // Translate function
     manager: EditorUIManager; // UI Manager instance for this editor
-    lastSelection: BaseSelection|null; // The last tracked selection made by the user
     options: Record<string, any>; // General user options which may be used by sub elements
 };
 
diff --git a/resources/js/wysiwyg/ui/framework/manager.ts b/resources/js/wysiwyg/ui/framework/manager.ts
index 92891b540..f10e85b47 100644
--- a/resources/js/wysiwyg/ui/framework/manager.ts
+++ b/resources/js/wysiwyg/ui/framework/manager.ts
@@ -5,6 +5,7 @@ import {$getSelection, BaseSelection, COMMAND_PRIORITY_LOW, LexicalEditor, SELEC
 import {DecoratorListener} from "lexical/LexicalEditor";
 import type {NodeKey} from "lexical/LexicalNode";
 import {EditorContextToolbar, EditorContextToolbarDefinition} from "./toolbars";
+import {getLastSelection, setLastSelection} from "../../utils/selection";
 
 export type SelectionChangeHandler = (selection: BaseSelection|null) => void;
 
@@ -108,8 +109,7 @@ export class EditorUIManager {
     }
 
     protected triggerStateUpdate(update: EditorUiStateUpdate): void {
-        const context = this.getContext();
-        context.lastSelection = update.selection;
+        setLastSelection(update.editor, update.selection);
         this.toolbar?.updateState(update);
         this.updateContextToolbars(update);
         for (const toolbar of this.activeContextToolbars) {
@@ -119,9 +119,10 @@ export class EditorUIManager {
     }
 
     triggerStateRefresh(): void {
+        const editor = this.getContext().editor;
         this.triggerStateUpdate({
-            editor: this.getContext().editor,
-            selection: this.getContext().lastSelection,
+            editor,
+            selection: getLastSelection(editor),
         });
     }
 
diff --git a/resources/js/wysiwyg/ui/index.ts b/resources/js/wysiwyg/ui/index.ts
index 5fbaec91b..116d6e1fc 100644
--- a/resources/js/wysiwyg/ui/index.ts
+++ b/resources/js/wysiwyg/ui/index.ts
@@ -21,7 +21,6 @@ export function buildEditorUI(container: HTMLElement, element: HTMLElement, scro
         scrollDOM: scrollContainer,
         manager,
         translate: (text: string): string => text,
-        lastSelection: null,
         options,
     };
     manager.setContext(context);
diff --git a/resources/js/wysiwyg/utils/diagrams.ts b/resources/js/wysiwyg/utils/diagrams.ts
index 2dee3ab6b..fb5543005 100644
--- a/resources/js/wysiwyg/utils/diagrams.ts
+++ b/resources/js/wysiwyg/utils/diagrams.ts
@@ -5,7 +5,7 @@ import * as DrawIO from "../../services/drawio";
 import {$createDiagramNode, DiagramNode} from "../nodes/diagram";
 import {ImageManager} from "../../components";
 import {EditorImageData} from "./images";
-import {$getNodeFromSelection} from "./selection";
+import {$getNodeFromSelection, getLastSelection} from "./selection";
 
 export function $isDiagramNode(node: LexicalNode | null | undefined): node is DiagramNode {
     return node instanceof DiagramNode;
@@ -80,7 +80,7 @@ export function showDiagramManager(callback: (image: EditorImageData) => any) {
 }
 
 export function showDiagramManagerForInsert(context: EditorUiContext) {
-    const selection = context.lastSelection;
+    const selection = getLastSelection(context.editor);
     showDiagramManager((image: EditorImageData) => {
         context.editor.update(() => {
             const diagramNode = $createDiagramNode(image.id, image.url);
diff --git a/resources/js/wysiwyg/utils/formats.ts b/resources/js/wysiwyg/utils/formats.ts
new file mode 100644
index 000000000..340be393d
--- /dev/null
+++ b/resources/js/wysiwyg/utils/formats.ts
@@ -0,0 +1,88 @@
+import {$isQuoteNode, HeadingNode, HeadingTagType} from "@lexical/rich-text";
+import {$getSelection, LexicalEditor, LexicalNode} from "lexical";
+import {
+    $getBlockElementNodesInSelection,
+    $getNodeFromSelection,
+    $insertNewBlockNodeAtSelection,
+    $toggleSelectionBlockNodeType,
+    getLastSelection
+} from "./selection";
+import {$createCustomHeadingNode, $isCustomHeadingNode} from "../nodes/custom-heading";
+import {$createCustomParagraphNode, $isCustomParagraphNode} from "../nodes/custom-paragraph";
+import {$createCustomQuoteNode} from "../nodes/custom-quote";
+import {$createCodeBlockNode, $isCodeBlockNode, $openCodeEditorForNode, CodeBlockNode} from "../nodes/code-block";
+import {$createCalloutNode, $isCalloutNode, CalloutCategory} from "../nodes/callout";
+
+const $isHeaderNodeOfTag = (node: LexicalNode | null | undefined, tag: HeadingTagType) => {
+    return $isCustomHeadingNode(node) && (node as HeadingNode).getTag() === tag;
+};
+
+export function toggleSelectionAsHeading(editor: LexicalEditor, tag: HeadingTagType) {
+    editor.update(() => {
+        $toggleSelectionBlockNodeType(
+            (node) => $isHeaderNodeOfTag(node, tag),
+            () => $createCustomHeadingNode(tag),
+        )
+    });
+}
+
+export function toggleSelectionAsParagraph(editor: LexicalEditor) {
+    editor.update(() => {
+        $toggleSelectionBlockNodeType($isCustomParagraphNode, $createCustomParagraphNode);
+    });
+}
+
+export function toggleSelectionAsBlockquote(editor: LexicalEditor) {
+    editor.update(() => {
+        $toggleSelectionBlockNodeType($isQuoteNode, $createCustomQuoteNode);
+    });
+}
+
+export function formatCodeBlock(editor: LexicalEditor) {
+    editor.getEditorState().read(() => {
+        const selection = $getSelection();
+        const lastSelection = getLastSelection(editor);
+        const codeBlock = $getNodeFromSelection(lastSelection, $isCodeBlockNode) as (CodeBlockNode | null);
+        if (codeBlock === null) {
+            editor.update(() => {
+                const codeBlock = $createCodeBlockNode();
+                codeBlock.setCode(selection?.getTextContent() || '');
+                $insertNewBlockNodeAtSelection(codeBlock, true);
+                $openCodeEditorForNode(editor, codeBlock);
+                codeBlock.selectStart();
+            });
+        } else {
+            $openCodeEditorForNode(editor, codeBlock);
+        }
+    });
+}
+
+export function cycleSelectionCalloutFormats(editor: LexicalEditor) {
+    editor.update(() => {
+        const selection = $getSelection();
+        const blocks = $getBlockElementNodesInSelection(selection);
+
+        let created = false;
+        for (const block of blocks) {
+            if (!$isCalloutNode(block)) {
+                block.replace($createCalloutNode('info'), true);
+                created = true;
+            }
+        }
+
+        if (created) {
+            return;
+        }
+
+        const types: CalloutCategory[] = ['info', 'warning', 'danger', 'success'];
+        for (const block of blocks) {
+            if ($isCalloutNode(block)) {
+                const type = block.getCategory();
+                const typeIndex = types.indexOf(type);
+                const newIndex = (typeIndex + 1) % types.length;
+                const newType = types[newIndex];
+                block.setCategory(newType);
+            }
+        }
+    });
+}
\ No newline at end of file
diff --git a/resources/js/wysiwyg/utils/selection.ts b/resources/js/wysiwyg/utils/selection.ts
index e34afbe36..74dd94527 100644
--- a/resources/js/wysiwyg/utils/selection.ts
+++ b/resources/js/wysiwyg/utils/selection.ts
@@ -8,7 +8,7 @@ import {
     $setSelection,
     BaseSelection,
     ElementFormatType,
-    ElementNode,
+    ElementNode, LexicalEditor,
     LexicalNode,
     TextFormatType
 } from "lexical";
@@ -17,6 +17,17 @@ import {LexicalElementNodeCreator, LexicalNodeMatcher} from "../nodes";
 import {$setBlocksType} from "@lexical/selection";
 
 import {$getParentOfType} from "./nodes";
+import {$createCustomParagraphNode} from "../nodes/custom-paragraph";
+
+const lastSelectionByEditor = new WeakMap<LexicalEditor, BaseSelection|null>;
+
+export function getLastSelection(editor: LexicalEditor): BaseSelection|null {
+    return lastSelectionByEditor.get(editor) || null;
+}
+
+export function setLastSelection(editor: LexicalEditor, selection: BaseSelection|null): void {
+    lastSelectionByEditor.set(editor, selection);
+}
 
 export function $selectionContainsNodeType(selection: BaseSelection | null, matcher: LexicalNodeMatcher): boolean {
     return $getNodeFromSelection(selection, matcher) !== null;
@@ -59,7 +70,7 @@ export function $toggleSelectionBlockNodeType(matcher: LexicalNodeMatcher, creat
     const selection = $getSelection();
     const blockElement = selection ? $getNearestBlockElementAncestorOrThrow(selection.getNodes()[0]) : null;
     if (selection && matcher(blockElement)) {
-        $setBlocksType(selection, $createParagraphNode);
+        $setBlocksType(selection, $createCustomParagraphNode);
     } else {
         $setBlocksType(selection, creator);
     }