Cover image for Writing a Firefox Web Extension using Vue.js

Writing a Firefox Web Extension using Vue.js

aligoren profile image Ali GOREN ・5 min read

Hi, It's been a long time since I was published my last tutorial post.

In this post, I'll explain how to write a web extension for Firefox using Vue.js.

Before starting, I'm sorry for my grammar mistakes.

In this example, we will create a to-do list app by overriding the browser's new tab.

Pre Requirements

You need to have knowledge about Vue to understand this post. But you don't need to Vue to create an extension like this. You can create one for yourself using Vanilla JS.

Creating Vue Project

vue create todo-list-extension

The name doesn't matter. I just like meaningful names. We will not use vuex or router. We will use localStorage as a database.

Replacing Default Component.

I'll replace the default component under the /src/components/ as TodoList. You also need to change its name in the App.vue


App.vue should be like this;

    <todo-list />

import TodoList from './components/TodoList.vue'
import './components/TodoList.css'

export default {
  name: 'app',
  components: {


I created a CSS file named TodoList.css in the components directory. The CSS will be like this. You can find this CSS if you googled for "Todo MVC"


Now we will create our application. Firstly, the template will be like this;

    <section class="todoapp">
      <header class="header">
        <h1>To Do List</h1>
        <input class="new-todo"
          autofocus autocomplete="off"
          placeholder="What needs to be done?"
      <section class="main" v-show="todos.length" >
        <input id="toggle-all" class="toggle-all" type="checkbox" v-model="allDone">
        <label for="toggle-all"></label>
        <ul class="todo-list">
          <li v-for="todo in filteredTodos"
            :class="{ completed: todo.completed, editing: todo == editedTodo }">
            <div class="view">
              <input class="toggle" type="checkbox" v-model="todo.completed">
              <label @dblclick="editTodo(todo)">{{ todo.title }}</label>
              <button class="destroy" @click="removeTodo(todo)"></button>
            <input class="edit" type="text"
              v-model="todo.title" v-todo-focus="todo == editedTodo"
              @keyup.esc="cancelEdit(todo)" />
      <footer class="footer" v-show="todos.length">
        <span class="todo-count">
          <strong>{{ remaining }}</strong> {{ remaining | pluralize }} left
        <ul class="filters">
          <li><a href="#" @click="filterTodos('all')" :class="{ selected: visibility == 'all' }">All</a></li>
          <li><a href="#" @click="filterTodos('active')" :class="{ selected: visibility == 'active' }">Active</a></li>
          <li><a href="#" @click="filterTodos('completed')" :class="{ selected: visibility == 'completed' }">Completed</a></li>
        <button class="clear-completed" @click="removeCompleted" v-show="todos.length > remaining">
          Clear completed
    <footer class="info">
      <p>Double-click to edit a todo</p>
      <p>Written by <a href="http://evanyou.me">Evan You</a></p>
      <p>Part of <a href="http://todomvc.com">TodoMVC</a></p>

After that, the component's script will be like this;

export default {
    name: 'TodoList',
    data() {
        return {
            newTodo: null,
            todos: [],
            filteredTodos: [],
            visibility: 'all',
            editedTodo: null,
            STORAGE_KEY: 'todo-list-v2'
    computed: {
        remaining: function() {
            return this.todos.filter(todo => !todo.completed).length
        allDone: {
            get: function() {
                return this.remaining === 0
            set: function(value) {
                this.todos.map(todo => todo.completed = value)

    mounted() {

        this.todos = JSON.parse(localStorage.getItem(this.STORAGE_KEY)) || []

    methods: {
        listTodos() {

            this.filteredTodos = []

            if (this.visibility == 'all') {
                this.todos.forEach(todo => {
            } else if(this.visibility == 'active') {
                this.todos.filter(todo => !todo.completed).forEach(todo => {
            } else if(this.visibility == 'completed') {
                this.todos.filter(todo => todo.completed).forEach(todo => {
        addTodo() {
                id: this.todos.length + 1,
                title: this.newTodo,
                completed: false

            localStorage.setItem(this.STORAGE_KEY, JSON.stringify(this.todos))


            this.newTodo = null
        editTodo(todo) {
            this.editedTodo = todo
        removeTodo(data) {
            this.todos = this.todos.filter(todo => todo.id != data.id)

            localStorage.setItem(this.STORAGE_KEY, JSON.stringify(this.todos))

        doneEdit() {

            localStorage.setItem(this.STORAGE_KEY, JSON.stringify(this.todos))

            this.editedTodo = null
        cancelEdit() {
            this.editedTodo = null
        removeCompleted() {
            this.todos = this.todos.filter(todo => !todo.completed)

            localStorage.setItem(this.STORAGE_KEY, JSON.stringify(this.todos))

        filterTodos(type) {

            this.visibility = type

    filters: {
        pluralize: function (n) {
            if (n <= 0) {
                return 'item'
            } else if(n === 1) {
                return 'item'

            return n === 1 ? 'item' : 'items'
    directives: {
        'todo-focus': function (el, binding) {
            if (binding.value) {

Actually, you can find a lot of example on Google for Todo MVC. This example one of these. So, I will not explain how methods work, what are the directives, filters or computed properties.

Building Vue Application

If you used Vue in your projects, you should know, Vue project's default output folder is dist folder.

By default, after yarn build command, the dist folder removes and re-creates To prevent this, we need to change the package.json file's script section like that.

"scripts": {
    "serve": "vue-cli-service serve",
    "build": "vue-cli-service build --no-clean",
    "lint": "vue-cli-service lint"

We just added the --no-clean flag for build operations. I'm doing this because I wanted to use the dist folder for this post.

yarn build

With this command, we've built our todo app.

manifest.json file

We'll create manifest.json file in the dist folder. Its content will be like this;

    "manifest_version": 2,
    "name": "To Do List",
    "version": "1.0.0",
    "chrome_url_overrides" : {
        "newtab": "index.html"

Using chrome_url_overrides key, you can override browser's default behavior for the new tab feature. That doesn't do that directly, it has subkey to do that. If you use newtab key, you can do it.

Debugging Web Extension

Okay, we finished everything, now let's open a new tab in our browser and typing this command to the address bar;


If you use any web extension, you will see them here.

If you see the **Load Temporary Add-on..." button, click it. It will open a file dialog. We need to select the manifest.json file we created.

If you didn't see any error, we'll see our extension in the extension dashboard.

Let's open a new tab :)


  • We learned how to write a basic Web Extension app for Firefox using Vue.js

  • We learned chrome_url_overrides key can be used by Firefox.

  • We learned if we want to override new tab we have to use chrome_url_overrides and newtab key.

Thanks for reading. I hope this helps you.

Posted on by:

aligoren profile



I'm a front-end developer. I'm living in Turkey. I started my professional career in 2016. I also interest in Backend, SQL technologies.


markdown guide

thanks for the awesome tutorial :)