diff --git a/common/web/keyboard-processor/src/index.ts b/common/web/keyboard-processor/src/index.ts
index 4187d7f366..b420a1a930 100644
--- a/common/web/keyboard-processor/src/index.ts
+++ b/common/web/keyboard-processor/src/index.ts
@@ -1,5 +1,7 @@
export * from "./keyboards/activeLayout.js";
export * from "./keyboards/defaultLayouts.js";
+export { default as DOMKeyboardLoader } from "./keyboards/domKeyboardLoader.js";
+export { default as DOMKeyboardSandbox } from "./keyboards/domKeyboardSandbox.js";
export { default as Keyboard } from "./keyboards/keyboard.js";
export * from "./keyboards/keyboard.js";
export { KeyboardHarness, KeyboardKeymanGlobal, MinimalCodesInterface, MinimalKeymanGlobal } from "./keyboards/keyboardHarness.js";
diff --git a/common/web/keyboard-processor/src/keyboards/domKeyboardLoader.ts b/common/web/keyboard-processor/src/keyboards/domKeyboardLoader.ts
new file mode 100644
index 0000000000..98d2ed193d
--- /dev/null
+++ b/common/web/keyboard-processor/src/keyboards/domKeyboardLoader.ts
@@ -0,0 +1,59 @@
+// Enables DOM types, but just for this one module.
+
+///
+
+import { KeyboardHarness, MinimalKeymanGlobal } from './keyboardHarness.js';
+
+import Keyboard from './keyboard.js';
+import KeyboardLoaderBase from './keyboardLoaderBase.js';
+
+import { ManagedPromise } from '@keymanapp/web-utils';
+
+export default class DOMKeyboardLoader extends KeyboardLoaderBase {
+ public readonly element: HTMLIFrameElement;
+
+ constructor()
+ constructor(harness: KeyboardHarness);
+ constructor(harness?: KeyboardHarness) {
+ if(harness && harness._jsGlobal != window) {
+ // Copy the String typing over; preserve string extensions!
+ harness._jsGlobal['String'] = window['String'];
+ }
+
+ if(!harness) {
+ super(new KeyboardHarness(window, MinimalKeymanGlobal));
+ } else {
+ super(harness);
+ }
+ }
+
+ protected loadKeyboardInternal(uri: string): Promise {
+ const promise = new ManagedPromise();
+
+ try {
+ const document = this.harness._jsGlobal.document;
+ const script = document.createElement('script');
+ document.head.appendChild(script);
+ script.onerror = promise.reject;
+ script.onload = () => {
+ if(this.harness.activeKeyboard) {
+ promise.resolve(this.harness.activeKeyboard);
+ } else {
+ promise.reject();
+ }
+ }
+
+ promise.finally(() => {
+ // https://stackoverflow.com/a/37393041 - totally safe.
+ script.remove();
+ });
+
+ // Now that EVERYTHING ELSE is ready, establish the link to the keyboard's script.
+ script.src = uri;
+ } catch (err) {
+ return Promise.reject(err);
+ }
+
+ return Promise.resolve(this.harness.activeKeyboard);
+ }
+}
\ No newline at end of file
diff --git a/common/web/keyboard-processor/src/keyboards/domKeyboardSandbox.ts b/common/web/keyboard-processor/src/keyboards/domKeyboardSandbox.ts
new file mode 100644
index 0000000000..7e098621b1
--- /dev/null
+++ b/common/web/keyboard-processor/src/keyboards/domKeyboardSandbox.ts
@@ -0,0 +1,35 @@
+// Enables DOM types, but just for this one module.
+///
+
+import { ManagedPromise } from "@keymanapp/web-utils";
+
+export default class DOMKeyboardSandbox {
+ public readonly sandboxHost: HTMLIFrameElement;
+ public sandbox(): Window {
+ return this.sandboxHost.contentWindow;
+ }
+
+ private constructor() {
+ const host = this.sandboxHost = document.createElement('iframe');
+ host.style.display = 'none';
+ host.style.width = '0px';
+ host.style.height = '0px';
+ host.tabIndex = -1;
+ host.title = "Sandboxed keyboard loader";
+ host.hidden = true;
+ }
+
+ public static buildSandbox(): Promise {
+ // Promise: because the iframe's document isn't instantly ready.
+ const promise = new ManagedPromise();
+ const instance = new DOMKeyboardSandbox();
+
+ instance.sandboxHost.onload = () => promise.resolve(instance);
+ instance.sandboxHost.onerror = () => promise.reject();
+
+ // Need to insert the sandbox into the DOM before it can load or error!
+ window.document.appendChild(instance.sandboxHost);
+
+ return promise.corePromise;
+ }
+}
\ No newline at end of file
diff --git a/common/web/keyboard-processor/src/keyboards/nodeKeyboardLoader.ts b/common/web/keyboard-processor/src/keyboards/nodeKeyboardLoader.ts
index f84eb4be63..e90636cc99 100644
--- a/common/web/keyboard-processor/src/keyboards/nodeKeyboardLoader.ts
+++ b/common/web/keyboard-processor/src/keyboards/nodeKeyboardLoader.ts
@@ -1,5 +1,3 @@
-// TODO: export from ../index.ts as well!
-
import { KeyboardHarness, MinimalKeymanGlobal } from './keyboardHarness.js';
import Keyboard from './keyboard.js';