diff --git a/README.md b/README.md
index fb25430a05aaf8427665d6c8a829d38e54a953c5..02b8d7259daa45c333dd659f50fe02079127f424 100644
--- a/README.md
+++ b/README.md
@@ -2,3 +2,14 @@
 
 ## Development
 `npm run serve`
+
+## Run the application as a web extension
+### Firefox
+* Build the packaged addon `npm run build`
+* Go to `about:debugging#/runtime/this-firefox`
+* Load `artifacts/virtual-keyboard-v<version>-production.zip` as a temporary extension
+
+### Chromium
+* Run `npm run serve` to support live reload
+* Go to `chrome://extensions/`
+* Click on "Load unpacked" and select the `dist` folder
diff --git a/src/components/KeyboardDisplay.vue b/src/components/KeyboardDisplay.vue
new file mode 100644
index 0000000000000000000000000000000000000000..61a9868300dc3805360eb78dc39e9f52501d7af5
--- /dev/null
+++ b/src/components/KeyboardDisplay.vue
@@ -0,0 +1,51 @@
+<template>
+  <div v-on:click.prevent class="virtual-keyboard">
+    <button v-on:mouseup="addChar('a')">Insert character "a"</button>
+    <button v-on:mouseup="addChar('b')">Insert character "b"</button>
+  </div>
+</template>
+
+<script>
+export default {
+  props: {
+    inputField: {
+      type: HTMLInputElement,
+      required: true,
+    },
+  },
+  mounted() {
+    this.inputField.onkeydown = (e) => {
+      const replacements = { a: "b" };
+      const character = replacements[e.key];
+      if (!character) return;
+      this.addChar(character);
+      return false;
+    };
+  },
+  methods: {
+    addChar(char) {
+      console.log("adding", char);
+      // Add a character to the input depending on selection position
+      const start = this.inputField.selectionStart;
+      const value = this.inputField.value;
+      this.inputField.value =
+        value.slice(0, start) +
+        char +
+        value.slice(this.inputField.selectionEnd);
+      // Reset the input caret after modifying the input value
+      this.inputField.selectionStart = this.inputField.selectionEnd = start + 1;
+    },
+  },
+};
+</script>
+
+<style lang="scss" scoped>
+.virtual-keyboard {
+  position: absolute;
+  top: 100%;
+  display: flex;
+}
+button {
+  padding: 1rem;
+}
+</style>
diff --git a/src/content/content.js b/src/content/content.js
index cbd64fabbfd8ac54ac0719b43bd6674cbce4be09..777fc1193817a2943ac7919361623327090e66df 100644
--- a/src/content/content.js
+++ b/src/content/content.js
@@ -1,58 +1,44 @@
-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);
+import Vue from "vue";
+import Keyboard from "../components/KeyboardDisplay.vue";
+import router from "../router";
+import store from "../store";
 
-let selectedInput = null;
+const inputFields = document.getElementsByTagName("input");
+let keyboard = 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;
+const createKeyboard = (input) => {
+  const keyboardDiv = document.createElement("div");
+  input.parentElement.appendChild(keyboardDiv);
+  keyboard = new Vue({
+    router,
+    store,
+    render: (h) => {
+      return h(Keyboard, {
+        props: { inputField: input },
+      });
+    },
+    el: keyboardDiv,
+  });
 };
 
 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;
-  }
+  // Handle the case where the input is already focused when this script is loaded
+  if (!keyboard && document.activeElement === input) createKeyboard(input);
+
   input.onfocus = () => {
-    input.parentElement.appendChild(keyboard);
-    selectedInput = input;
+    // TODO it is not possible to go from an input field to another with the keyboard global
+    if (!keyboard) createKeyboard(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;
-    }
+  input.onblur = (e) => {
+    if (!keyboard) return;
+    // This is a hacky way to keep the focus on the input
+    setTimeout(() => {
+      if (keyboard.$el.contains(document.activeElement)) e.target.focus();
+      else {
+        keyboard.$destroy();
+        keyboard.$el.parentElement.removeChild(keyboard.$el);
+        keyboard = null;
+      }
+    }, 1);
   };
 }
-
-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 e986c126a876131821e8e127955e93cd6e5fe198..7962da2532777676820dd396a9ce0bb2c2b0e1d6 100644
--- a/src/manifest.json
+++ b/src/manifest.json
@@ -28,8 +28,7 @@
   "content_scripts": [
     {
       "matches": ["<all_urls>"],
-      "js": ["js/content.js"],
-      "css": ["css/content.css"]
+      "js": ["js/content.js"]
     }
   ]
 }