top of page

Building a ToDo List Application With Vue.JS

Updated: May 30, 2019


In this series we will build a browser-based ToDo List application using Vue.js and Bulma. We will learn how to create a new Vue project, how to add dependencies to the project, how to use Vue's powerful reactivity, and how to wrap everything around a responsive layout with Bulma.

This tutorial will start with some of the very basic Vue features, with the goal of progressively enhancing our app as we go along.


Vue.js is a modern, progressive and open-source JavaScript framework. It is flexible enough that it can add to (or replace) jQuery projects, but can also be used to build a full-fledged SPA (Single Page Application).

Vue.js is less opinionated than competing frameworks, but also lighter and faster than its main competitors. Currently, Vue.js is the fastest growing JS framework in popularity, in no small part due to its enthusiastic community and excellent documentation. Vue is heavily inspired by the strengths of AngularJS and React, but builds and improves upon those frameworks while minimizing some of their weaknesses.

Finally, for those who are not big fans of TypeScript, Vue does not enforce it like other frameworks, but it offers support for it, so you are free to code however it suits you.


Bulma is a modern responsive css framework and an excellent alternative to more traditional frameworks such as Bootstrap and Foundation. It was built from the ground up to be responsive and very lightweight.


For this series, we will need a series of tools and utilities but I won't go into detail as to how to install them. There's plenty of documentation out there without adding more bulk to this article.

gitNPMNVM (optional but nice to have)Yarn I prefer Yarn instead of npm for package management but that's up to youvue-cli vue-cli is Vue.js' command-line utility that makes it easy to scaffold a new project.

If you would like to follow along, you can clone the application repository with this command:

git clone

The repository has been tagged at various points to correspond with stages of progress in the blog post.


We will use vue-cli to quickly scaffold our new app.

Note vue-cli allows us to specify what templates we want to use as a baseline for our project. One of the most popular module bundlers for Vue apps is webpack, and that's what we shall use as a template.

To create your new app, go to the folder where you want it to reside and run the following command (I will call the app vue-todo-client):

vue init webpack vue-todo-client

You will be asked a series of questions about the project such as project and author name, etc. You will want to accept runtime+compiler build and vue-router. As for the ESLint preset, I chose Standard for this project. For the next 2 questions regarding unit and e2e tests I answered No, as I will not cover testing in this series.

This will generate a directory with the complete Vue structure that we can use to build our SPA.

Now switch to the newly created directory and call yarn to install all the dependencies.


Our new Vue app can be run from the command line using:

yarn run dev

The app should open automatically in your browser or you can access it directly at http://localhost:8080

Note: You may run into a conflict if you have other local apps running on port 8080. To fix that, open config/index.js and change the port in the dev object to something else. I changed mine to 8089:

 dev: {
    env: require('./dev.env'),
    port: 8089,

You will be presented with the standard Vue intro template.


Looking at the folder structure for the newly created project, our main focus is inside the src directory, where the actual source code for your app lives.

Vue.js organizes code in logical structures called components. Components go a little against the classic MVC paradigm, in that business logic, presentation, and behavior are not separated, but instead reside together inside a component to form a logical whole.

The standard naming scheme for components is Component.vue or MyComponent.vue. The entrypoint to a Vue app is called App.vue by default.

Another point of interest is index.js inside the router directory. This is where we define the routes for our app. Looking inside this file we can see that the / route points to the Hello component. In other words, if we access the root URL for our app, Vue will load components/Hello.vue. Notice that the components/ part is implied when defining routes.

Note Hello.vue is named HelloWorld.vue in newer versions of vue-cli. You can read more about component naming conventions in the Vue style guide.


For our ToDo List, at the most basic level we are going to need a text box to save our list.

Let's start by opening Hello.vue and removing all the code inside the <template> tags, leaving only the following:

  <div class="hello">
    <h1>{{ msg }}</h1>

Now, if you have run yarn run dev (if not, run it), any change you make to the code will update automatically in the browser. We should now see the Vue logo and the text "Welcome to Your Vue.js App." Let's change that.

In Hello.vue change the msg property of the data object:

export default {
  name: 'hello',
  data () {
    return {
      msg: 'ToDo List'

Notice how our title is bound to the value of msg by using the handlebars syntax {{ msg }}. As we change this value, our view in the browser will update.

Next, let's add a text box and bind this to a label. There are two parts to this. First, add an empty property label to the data object:

 data () {
    return {
      msg: 'ToDo List',
      label: ''

Second, add some HTML (after the h1 element, but before the closing div tag):

<label><input v-model="label" />{{ label }}</label>

Notice the attribute v-model. This is one of the basic directives that Vue makes heavy use of. Those familiar with AngularJS will immediately make an association with Angular's ng- style directives.

v-model is the magic that confers 2-way binding to our input element. As we type inside the text box we notice that the label is updated automatically. No complicated JS required!

At this point you can check out the code with tag 0.1.1:

git checkout v0.1.1


The above exercise was just a quick way to demonstrate 2-way binding. For our ToDo List, however, we will require a, well, list. So let's go ahead and build a static list of items (and change a few other things in the process). Our new HTML:

<h1>{{ title }}</h1>

<label>Add a new task <input v-model="newTask" /></label>

  <li>Task 1</li>
  <li>Task 2</li>
  <li>Task 3</li>

I also changed the data object to match:

return {
  title: 'ToDo List',
  newTask: ''

Let's make this task list dynamic. We can do that by binding it to a new array object, let's call it tasks. This object will contain our tasks which are themselves objects (we can add various useful properties to them, such as whether the item was completed):

return {
  title: 'ToDo List',
  newTask: '',
  tasks: [
    { description: 'A task to do', completed: false },
    { description: 'Do another thing', completed: false },
    { description: 'Do some more stuff', completed: false }

We can now iterate through the tasks object using another Vue directive called v-for:

<ul v-for="task in tasks">
  <li>{{ task.description }}</li>

Going to our browser we can see how the list is now rendered based on the contents of our new object.

At this point some styling might be in order, since our task list doesn't look great. I went back to App.vue and removed the Vue logo from the top of the page, as well as made the list items display vertically.

You can checkout the code up to this point at version 0.2:

git checkout v0.2


We want to be able to add new tasks using the text box we previously created. The new task will be added as an object to our existing tasks. Let's add a button and some logic to it. The goal is that, when we enter some text and click the button, we are presented with an alert containing our text.

Add the following HTML (after the input element, but before the closing label tag):

<button v-on:click="addTask()">Add</button>

A new directive to handle events was introduced: v-on. Those familiar with jQuery will find a lot of similarities. In this case we give it an event click and the name of the method, addTask, that handles the event.

The Vue instance object now contains a new child, called methods. This is a Vue reserved word under which all our methods (or functions) for the current component are defined.

export default {
  name: 'todo',
  data () {
  methods: {
    addTask: function () {

Here, we are simply alerting the value of the newTask variable. Notice how we use this to access the scope of the Vue object. The following code would throw an undefined error.

methods: {
  addTask: function () {

Displaying the new task in an alert is not very useful so we obviously want to add it to our existing tasks object.

Our addTask method changes to:

addTask: function () {
  this.tasks.push({description: this.newTask, completed: false})

The above code pushes a new object in the existing tasks object. As already mentioned, this needs to be an object because we include relevant task data along with the description, such as completed, which by default is false for any new task.

If we go back to the browser and add a new task, the new task is added automatically to the bottom of the list. This is there Vue's reactivity comes in. No other JS is needed. Neat, huh?

You can checkout the code up to this point at version 0.3:

git checkout v0.3


Let's clean up our list. We want to start adding to an empty list, but when the list is empty it would be nice to display a message instead of showing empty space. For this we will make use of conditional rendering.

We don't need a pre-populated tasks array anymore so we can initialize that to be empty.

tasks: []

A new Vue directive pair allows us to apply conditional rendering. In other words, if a certain condition is met, show this thing, otherwise show another thing. The new directives are v-if and v-else. Here's how our HTML looks now:

<p v-if="tasks.length">
  <ul v-for="task in tasks">
    <li>{{ task.description }}</li>
<p v-else>
  There are no tasks yet

For conditional rendering to work properly, the two chunks of code must be wrapped in identical tags. In this case I chose to wrap both the list and the message in a paragraph, but you can use divs or anything really.

v-if is passed a value or an expression, in this case tasks.length, and it will render the HTML inside the first p if the expression resolves to true.

Optionally, v-if is paired with an v-else (which in this case does not take any arguments), to render something based on the false condition. Since, when the page is loaded, the tasks array is empty by default, the expression will return false and the paragraph with the v-else will render instead.

Notice how Vue handles rendering automatically, without requiring us to build any more logic. As soon as an item is added, it will replace the empty state message. When the last item is removed, the empty message will appear again, although we will get to this later.

One other minor enhancement is that once the Add button is clicked and the new item is added to the list, we want the text box to be cleared. Simple, initialize newTask to an empty string at the end of the addTask method:

addTask: function () {
  this.tasks.push({description: this.newTask, completed: false})
  this.newTask = ''

Because newTask is 2-way bound to the text box, the latter will be cleared automatically when we initialize the variable.

You can checkout the code up to this point at version 0.4:

git checkout v0.4


A ToDo List is not a ToDo List if we can't mark items as done, so let's add some checkboxes to our list.

<li><input type="checkbox"> {{ task.description }}</li>

A change of perspective is needed at this point. Although we are displaying the items that are in the task array, it would be useful to have a way to filter completed and to-be-completed items. That's where computed properties come in handy. A computed property is an operation that is applied frequently to an object, expressed as a method.

Like the methods collection we've been using, computed is another Vue-specific object that is used to group computed properties together. Let's add the following code right after methods:

computed: {
  completedTasks: function () {
    return this.tasks.filter(task => task.completed)
  incompleteTasks: function () {
    return this.tasks.filter(task => !task.completed)

We have 2 properties completedTasks and incompleteTasks. We are simply applying a filter to the tasks array to return only those tasks that are completed or vice-versa. If you find this code unfamiliar, it's because this is a ES6-style shortcut that can be expanded as:

return this.tasks.filter(function(task) { return task.completed })

Either style works, but you'll see me use ES6 more frequently as I try to make the code more future-proof.

Now that we have these 2 new computed properties, we can use them to separate completed and incomplete tasks, both visually and logically.

Instead of iterating through all our tasks, we will split this into 2 loops that will each act on the new computed properties.

<div v-if="tasks.length">
  <p v-if="incompleteTasks.length">
    <ul v-for="task in incompleteTasks">
      <li><input type="checkbox"> {{ task.description }}</li>
  <p v-else>
    All tasks have been completed!
  <p v-if="completedTasks.length">
    <ul v-for="task in completedTasks">
      <li><input type="checkbox"> {{ task.description }}</li>
<div v-else>
  There are no tasks yet

This works because, remember, completedTasks and incompleteTasks are arrays, just as tasks.

Some code organization is needed. We are now wrapping the incomplete and complete tasks inside a div because the conditional rendering must apply to the full tasks array and we still want to show a message when there are no tasks. But we only want to show a message for incompleteTasks when this object is empty (all the tasks were completed).

Now that we've separated the completed from the incomplete tasks, all that remains is to add some behavior to the checkboxes. Let's bind them to the computed property of each item, for both completedTasks and incompleteTasks.

<li><input type="checkbox" v-model="task.completed"> {{ task.description }}</li>

Reactivity ensures that as we check off items from the top (incomplete), they are automatically removed from the incompleteTasks array and added to the bottom (rendered from completedTasks).

When all the tasks have been completed, this is what we see:

You can checkout the code up to this point at version 0.5:

git checkout v0.5


While any modern CSS framework works just as well, the Vue community is rather partial to Bulma, and I happen to like it as well. Let's go ahead and integrate Bulma into our app, along with Font Awesome (a CSS glyph library). Run the following command:

yarn add bulma font-awesome

In App.vue add the following:

<style lang="sass" src="bulma"></style>
<style lang="sass">
  $fa-font-path: '../node_modules/font-awesome/fonts/';
  @import '../node_modules/font-awesome/scss/font-awesome';

If you receive an error that a certain module is missing, run yarn add <missing-module>. In my case sass-loader, css-loader, and vue-style-loader were missing so I ran:

yarn add sass-loader css-loader vue-style-loader

Finally, node-sass was also required. However, because Node is sometimes a pain to use, I first had to switch to the correct version (from 4.3.2 to 7.8.0). Then I restarted the local dev server for the changes to take effect.

nvm use v7.8.0
yarn add node-sass
yarn run dev

Let's make use of Bulma to make our app look good. I'm particularly fond of Bulma's panel component so I'm going to convert the existing code to that.

I won't explain every change, but since we're at the end of the first part of this series I will let you explore the HTML yourself by checking out v1.0 of our app.

git checkout v1.0

This is what you should see so far (mobile view shown).


In this first part of my Vue.js + Bulma series I have shown how to:

Create a new Vue.js projectWork with Vue's methods and computed propertiesMake use of 2-way bindingRender a list dynamicallyRender content conditionallyIntegrate a CSS framework and make the app responsive

1,689 views0 comments

Recent Posts

See All


bottom of page