diff --git a/resources/js/wysiwyg/helpers.ts b/resources/js/wysiwyg/helpers.ts
index 7218f1ae6..40379cc27 100644
--- a/resources/js/wysiwyg/helpers.ts
+++ b/resources/js/wysiwyg/helpers.ts
@@ -3,7 +3,7 @@ import {
     $getSelection,
     $isTextNode,
     BaseSelection,
-    LexicalEditor, TextFormatType
+    LexicalEditor, LexicalNode, TextFormatType
 } from "lexical";
 import {LexicalElementNodeCreator, LexicalNodeMatcher} from "./nodes";
 import {$getNearestBlockElementAncestorOrThrow} from "@lexical/utils";
@@ -28,23 +28,27 @@ export function el(tag: string, attrs: Record<string, string> = {}, children: (s
 }
 
 export function selectionContainsNodeType(selection: BaseSelection|null, matcher: LexicalNodeMatcher): boolean {
+    return getNodeFromSelection(selection, matcher) !== null;
+}
+
+export function getNodeFromSelection(selection: BaseSelection|null, matcher: LexicalNodeMatcher): LexicalNode|null {
     if (!selection) {
-        return false;
+        return null;
     }
 
     for (const node of selection.getNodes()) {
         if (matcher(node)) {
-            return true;
+            return node;
         }
 
         for (const parent of node.getParents()) {
             if (matcher(parent)) {
-                return true;
+                return parent;
             }
         }
     }
 
-    return false;
+    return null;
 }
 
 export function selectionContainsTextFormat(selection: BaseSelection|null, format: TextFormatType): boolean {
diff --git a/resources/js/wysiwyg/ui/defaults/button-definitions.ts b/resources/js/wysiwyg/ui/defaults/button-definitions.ts
index da0a1e2c5..f5be82519 100644
--- a/resources/js/wysiwyg/ui/defaults/button-definitions.ts
+++ b/resources/js/wysiwyg/ui/defaults/button-definitions.ts
@@ -1,13 +1,19 @@
 import {EditorButtonDefinition} from "../framework/buttons";
 import {
-    $createParagraphNode,
-    $isParagraphNode,
+    $createNodeSelection,
+    $createParagraphNode, $getSelection,
+    $isParagraphNode, $setSelection,
     BaseSelection, FORMAT_TEXT_COMMAND,
     LexicalNode,
     REDO_COMMAND, TextFormatType,
     UNDO_COMMAND
 } from "lexical";
-import {selectionContainsNodeType, selectionContainsTextFormat, toggleSelectionBlockNodeType} from "../../helpers";
+import {
+    getNodeFromSelection,
+    selectionContainsNodeType,
+    selectionContainsTextFormat,
+    toggleSelectionBlockNodeType
+} from "../../helpers";
 import {$createCalloutNode, $isCalloutNodeOfCategory, CalloutCategory} from "../../nodes/callout";
 import {
     $createHeadingNode,
@@ -17,7 +23,7 @@ import {
     HeadingNode,
     HeadingTagType
 } from "@lexical/rich-text";
-import {$isLinkNode, $toggleLink} from "@lexical/link";
+import {$isLinkNode, $toggleLink, LinkNode} from "@lexical/link";
 import {EditorUiContext} from "../framework/core";
 
 export const undo: EditorButtonDefinition = {
@@ -133,9 +139,29 @@ export const code: EditorButtonDefinition = buildFormatButton('Inline Code', 'co
 export const link: EditorButtonDefinition = {
     label: 'Insert/edit link',
     action(context: EditorUiContext) {
-        context.editor.update(() => {
-            $toggleLink('http://example.com');
-        })
+        const linkModal = context.manager.createModal('link');
+        context.editor.getEditorState().read(() => {
+            const selection = $getSelection();
+            const selectedLink = getNodeFromSelection(selection, $isLinkNode) as LinkNode|null;
+
+            let formDefaults = {};
+            if (selectedLink) {
+                formDefaults = {
+                    url: selectedLink.getURL(),
+                    text: selectedLink.getTextContent(),
+                    title: selectedLink.getTitle(),
+                    target: selectedLink.getTarget(),
+                }
+
+                context.editor.update(() => {
+                    const selection = $createNodeSelection();
+                    selection.add(selectedLink.getKey());
+                    $setSelection(selection);
+                });
+            }
+
+            linkModal.show(formDefaults);
+        });
     },
     isActive(selection: BaseSelection|null): boolean {
         return selectionContainsNodeType(selection, $isLinkNode);
diff --git a/resources/js/wysiwyg/ui/defaults/form-definitions.ts b/resources/js/wysiwyg/ui/defaults/form-definitions.ts
index c8477d9f2..457efa421 100644
--- a/resources/js/wysiwyg/ui/defaults/form-definitions.ts
+++ b/resources/js/wysiwyg/ui/defaults/form-definitions.ts
@@ -1,19 +1,26 @@
-import {EditorFormDefinition, EditorFormFieldDefinition, EditorSelectFormFieldDefinition} from "../framework/forms";
+import {EditorFormDefinition, EditorSelectFormFieldDefinition} from "../framework/forms";
 import {EditorUiContext} from "../framework/core";
+import {$createLinkNode} from "@lexical/link";
+import {$createTextNode, $getSelection} from "lexical";
 
 
 export const link: EditorFormDefinition = {
     submitText: 'Apply',
-    cancelText: 'Cancel',
     action(formData, context: EditorUiContext) {
-        // Todo
-        console.log('link-form-action', formData);
+        context.editor.update(() => {
+
+            const selection = $getSelection();
+
+            const linkNode = $createLinkNode(formData.get('url')?.toString() || '', {
+                title: formData.get('title')?.toString() || '',
+                target: formData.get('target')?.toString() || '',
+            });
+            linkNode.append($createTextNode(formData.get('text')?.toString() || ''));
+
+            selection?.insertNodes([linkNode]);
+        });
         return true;
     },
-    cancel() {
-        // Todo
-        console.log('link-form-cancel');
-    },
     fields: [
         {
             label: 'URL',
diff --git a/resources/js/wysiwyg/ui/framework/forms.ts b/resources/js/wysiwyg/ui/framework/forms.ts
index 0fce73c12..c6338f798 100644
--- a/resources/js/wysiwyg/ui/framework/forms.ts
+++ b/resources/js/wysiwyg/ui/framework/forms.ts
@@ -15,9 +15,7 @@ export interface EditorSelectFormFieldDefinition extends EditorFormFieldDefiniti
 
 export interface EditorFormDefinition {
     submitText: string;
-    cancelText: string;
     action: (formData: FormData, context: EditorUiContext) => boolean;
-    cancel: () => void;
     fields: EditorFormFieldDefinition[];
 }
 
@@ -29,6 +27,15 @@ export class EditorFormField extends EditorUiElement {
         this.definition = definition;
     }
 
+    setValue(value: string) {
+        const input = this.getDOMElement().querySelector('input,select') as HTMLInputElement;
+        input.value = value;
+    }
+
+    getName(): string {
+        return this.definition.name;
+    }
+
     protected buildDOM(): HTMLElement {
         const id = `editor-form-field-${this.definition.name}-${Date.now()}`;
         let input: HTMLElement;
@@ -51,14 +58,38 @@ export class EditorFormField extends EditorUiElement {
 
 export class EditorForm extends EditorContainerUiElement {
     protected definition: EditorFormDefinition;
+    protected onCancel: null|(() => void) = null;
 
     constructor(definition: EditorFormDefinition) {
         super(definition.fields.map(fieldDefinition => new EditorFormField(fieldDefinition)));
         this.definition = definition;
     }
 
+    setValues(values: Record<string, string>) {
+        for (const name of Object.keys(values)) {
+            const field = this.getFieldByName(name);
+            if (field) {
+                field.setValue(values[name]);
+            }
+        }
+    }
+
+    setOnCancel(callback: () => void) {
+        this.onCancel = callback;
+    }
+
+    protected getFieldByName(name: string): EditorFormField|null {
+        for (const child of this.children as EditorFormField[]) {
+            if (child.getName() === name) {
+                return child;
+            }
+        }
+
+        return null;
+    }
+
     protected buildDOM(): HTMLElement {
-        const cancelButton = el('button', {type: 'button', class: 'editor-form-action-secondary'}, [this.trans(this.definition.cancelText)]);
+        const cancelButton = el('button', {type: 'button', class: 'editor-form-action-secondary'}, [this.trans('Cancel')]);
         const form = el('form', {}, [
             ...this.children.map(child => child.getDOMElement()),
             el('div', {class: 'editor-form-actions'}, [
@@ -74,7 +105,9 @@ export class EditorForm extends EditorContainerUiElement {
         });
 
         cancelButton.addEventListener('click', (event) => {
-            this.definition.cancel();
+            if (this.onCancel) {
+                this.onCancel();
+            }
         });
 
         return form;
diff --git a/resources/js/wysiwyg/ui/framework/manager.ts b/resources/js/wysiwyg/ui/framework/manager.ts
index f1a34c92a..c3fe9ecd8 100644
--- a/resources/js/wysiwyg/ui/framework/manager.ts
+++ b/resources/js/wysiwyg/ui/framework/manager.ts
@@ -1,11 +1,38 @@
-
-
-
+import {EditorFormModal, EditorFormModalDefinition} from "./modals";
+import {EditorUiContext} from "./core";
 
 
 export class EditorUIManager {
 
-    // Todo - Register and show modal via this
-    //  (Part of UI context)
+    protected modalDefinitionsByKey: Record<string, EditorFormModalDefinition> = {};
+    protected context: EditorUiContext|null = null;
+
+    setContext(context: EditorUiContext) {
+        this.context = context;
+    }
+
+    getContext(): EditorUiContext {
+        if (this.context === null) {
+            throw new Error(`Context attempted to be used without being set`);
+        }
+
+        return this.context;
+    }
+
+    registerModal(key: string, modalDefinition: EditorFormModalDefinition) {
+        this.modalDefinitionsByKey[key] = modalDefinition;
+    }
+
+    createModal(key: string): EditorFormModal {
+        const modalDefinition = this.modalDefinitionsByKey[key];
+        if (!modalDefinition) {
+            console.error(`Attempted to show modal of key [${key}] but no modal registered for that key`);
+        }
+
+        const modal = new EditorFormModal(modalDefinition);
+        modal.setContext(this.getContext());
+
+        return modal;
+    }
 
 }
\ No newline at end of file
diff --git a/resources/js/wysiwyg/ui/framework/modals.ts b/resources/js/wysiwyg/ui/framework/modals.ts
new file mode 100644
index 000000000..e2a6b3f33
--- /dev/null
+++ b/resources/js/wysiwyg/ui/framework/modals.ts
@@ -0,0 +1,63 @@
+import {EditorForm, EditorFormDefinition} from "./forms";
+import {el} from "../../helpers";
+import {EditorContainerUiElement} from "./containers";
+
+
+export interface EditorModalDefinition {
+    title: string;
+}
+
+export interface EditorFormModalDefinition extends EditorModalDefinition {
+    form: EditorFormDefinition;
+}
+
+export class EditorFormModal extends EditorContainerUiElement {
+    protected definition: EditorFormModalDefinition;
+
+    constructor(definition: EditorFormModalDefinition) {
+        super([new EditorForm(definition.form)]);
+        this.definition = definition;
+    }
+
+    show(defaultValues: Record<string, string>) {
+        const dom = this.getDOMElement();
+        document.body.append(dom);
+
+        const form = this.getForm();
+        form.setValues(defaultValues);
+        form.setOnCancel(this.hide.bind(this));
+    }
+
+    hide() {
+        this.getDOMElement().remove();
+    }
+
+    protected getForm(): EditorForm {
+        return this.children[0] as EditorForm;
+    }
+
+    protected buildDOM(): HTMLElement {
+        const closeButton = el('button', {class: 'editor-modal-close', type: 'button', title: this.trans('Close')}, ['x']);
+        closeButton.addEventListener('click', this.hide.bind(this));
+
+        const modal = el('div', {class: 'editor-modal editor-form-modal'}, [
+            el('div', {class: 'editor-modal-header'}, [
+                el('div', {class: 'editor-modal-title'}, [this.trans(this.definition.title)]),
+                closeButton,
+            ]),
+            el('div', {class: 'editor-modal-body'}, [
+                this.getForm().getDOMElement(),
+            ]),
+        ]);
+
+        const wrapper = el('div', {class: 'editor-modal-wrapper'}, [modal]);
+
+        wrapper.addEventListener('click', event => {
+            if (event.target && !modal.contains(event.target as HTMLElement)) {
+                this.hide();
+            }
+        });
+
+        return wrapper;
+    }
+}
\ No newline at end of file
diff --git a/resources/js/wysiwyg/ui/index.ts b/resources/js/wysiwyg/ui/index.ts
index 5a0d7fd2d..7e1f8d981 100644
--- a/resources/js/wysiwyg/ui/index.ts
+++ b/resources/js/wysiwyg/ui/index.ts
@@ -6,8 +6,7 @@ import {
 } from "lexical";
 import {getMainEditorFullToolbar} from "./toolbars";
 import {EditorUIManager} from "./framework/manager";
-import {EditorForm} from "./framework/forms";
-import {link} from "./defaults/form-definitions";
+import {link as linkFormDefinition} from "./defaults/form-definitions";
 
 export function buildEditorUI(element: HTMLElement, editor: LexicalEditor) {
     const manager = new EditorUIManager();
@@ -16,16 +15,18 @@ export function buildEditorUI(element: HTMLElement, editor: LexicalEditor) {
         manager,
         translate: (text: string): string => text,
     };
+    manager.setContext(context);
 
     // Create primary toolbar
     const toolbar = getMainEditorFullToolbar();
     toolbar.setContext(context);
     element.before(toolbar.getDOMElement());
 
-    // Form test
-    const linkForm = new EditorForm(link);
-    linkForm.setContext(context);
-    element.before(linkForm.getDOMElement());
+    // Register modals
+    manager.registerModal('link', {
+        title: 'Insert/Edit link',
+        form: linkFormDefinition,
+    });
 
     // Update button states on editor selection change
     editor.registerCommand(SELECTION_CHANGE_COMMAND, () => {
diff --git a/resources/sass/_editor.scss b/resources/sass/_editor.scss
index 48912be8b..2633e8539 100644
--- a/resources/sass/_editor.scss
+++ b/resources/sass/_editor.scss
@@ -46,4 +46,30 @@
 
 .editor-format-menu .editor-dropdown-menu {
   min-width: 320px;
+}
+
+// Modals
+.editor-modal-wrapper {
+  position: fixed;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  z-index: 999;
+  background-color: rgba(0, 0, 0, 0.5);
+  width: 100%;
+  height: 100%;
+}
+.editor-modal {
+  background-color: #FFF;
+  border: 1px solid #DDD;
+  padding: 1rem;
+  border-radius: 4px;
+}
+.editor-modal-header {
+  display: flex;
+  justify-content: space-between;
+  margin-bottom: 1rem;
+}
+.editor-modal-title {
+  font-weight: 700;
 }
\ No newline at end of file