diff --git a/src/content/content.js b/src/content/content.js
new file mode 100644
index 0000000000000000000000000000000000000000..cbd64fabbfd8ac54ac0719b43bd6674cbce4be09
--- /dev/null
+++ b/src/content/content.js
@@ -0,0 +1,58 @@
+const inputFields = document.getElementsByTagName("input");
+const replacements = {
+  a: "b",
+};
+// Create a simple keyboard
+const buttonA = document.createElement("button");
+buttonA.textContent = 'Insert "a"';
+const buttonB = document.createElement("button");
+buttonB.textContent = 'Insert "b"';
+const keyboard = document.createElement("div");
+keyboard.appendChild(buttonA);
+keyboard.appendChild(buttonB);
+
+let selectedInput = null;
+
+const addChar = (i, char) => {
+  const start = i.selectionStart;
+  i.value = i.value.slice(0, start) + char + i.value.slice(i.selectionEnd);
+  // Updating the input value moves the caret to the end by default
+  i.selectionStart = i.selectionEnd = start + 1;
+};
+
+for (const input of inputFields) {
+  input.onkeydown = (e) => {
+    const character = replacements[e.key];
+    if (!character) return;
+    addChar(input, character);
+    return false;
+  };
+  // Handle the case where the input is already focused
+  if (document.activeElement === input) {
+    input.parentElement.appendChild(keyboard);
+    selectedInput = input;
+  }
+  input.onfocus = () => {
+    input.parentElement.appendChild(keyboard);
+    selectedInput = input;
+  };
+  // https://css-tricks.com/a-css-approach-to-trap-focus-inside-of-an-element/
+  input.ontransitionend = (e) => {
+    if (input.matches(":focus") || keyboard.contains(document.activeElement))
+      e.target.focus();
+    else {
+      input.parentElement.removeChild(keyboard);
+      selectedInput = null;
+    }
+  };
+}
+
+keyboard.onclick = () => {
+  return false;
+};
+buttonA.onmouseup = () => {
+  if (selectedInput) addChar(selectedInput, "a");
+};
+buttonB.onmouseup = () => {
+  if (selectedInput) addChar(selectedInput, "b");
+};
diff --git a/src/manifest.json b/src/manifest.json
index 98a6e854296afd6d6cbde023e75ddfb1ca958e47..e986c126a876131821e8e127955e93cd6e5fe198 100644
--- a/src/manifest.json
+++ b/src/manifest.json
@@ -24,5 +24,12 @@
   "options_ui": {
     "page": "options.html",
     "browser_style": true
-  }
+  },
+  "content_scripts": [
+    {
+      "matches": ["<all_urls>"],
+      "js": ["js/content.js"],
+      "css": ["css/content.css"]
+    }
+  ]
 }
diff --git a/vue.config.js b/vue.config.js
index dec60c63a5c8438c3e0376ea76a90f07c9ab5408..71c2d08a1f8de8bfe467be32c02a7fc18968e333 100644
--- a/vue.config.js
+++ b/vue.config.js
@@ -13,6 +13,17 @@ module.exports = {
     },
   },
   pluginOptions: {
-    browserExtension: {},
+    browserExtension: {
+      components: {
+        contentScripts: true,
+      },
+      componentOptions: {
+        contentScripts: {
+          entries: {
+            content: "src/content/content.js",
+          },
+        },
+      },
+    },
   },
 };