7 days of unlimited WordPress themes, plugins & graphics - for free!* Unlimited asset downloads! Start 7-Day Free Trial
  1. Web Design
  2. JavaScript

How to Build a To-Do App With Vanilla JavaScript (and Local Storage)

Scroll to top
Read Time: 13 mins

In this tutorial, we’ll enhance our front-end skills by learning to build a “handmade” to-do app. To create it, we won’t take advantage of any JavaScript frameworks; we’ll just use HTML, CSS, and vanilla JavaScript.

What We’ll be Building

Here’s an introductory video which demonstrates the functionality of the JavaScript app that we’re going to create. Users will be able to add tasks, mark them as complete, and remove them. Task totals and their statuses will be shown on the status bar:

Here’s the demo on Codepen for you to fork and play with:

Note: This isn’t an introductory tutorial. It assumes that you are familiar with essential front-end skills like CSS Grid, flexbox, ES6, JavaScript arrays, etc. Also, for this demonstration, making the app fully accessible isn’t a priority.

1. Begin With the Required Assets

To make the layout a bit more unique, we’ll use some handmade SVG illustrations and a custom font taken from Envato Elements.

SUMMER - FONT PACK on Envato Elements

It’s worth noting that most of these assets will come from a previous tutorial. In actual fact, we’ll also use a lot of the positioning techniques that we learned in this tutorial, so it’s worth reading it.

How to Build a Responsive Handmade SVG FormHow to Build a Responsive Handmade SVG FormHow to Build a Responsive Handmade SVG Form

2. Continue With the Page Markup

We’ll start with an SVG and a div container:

SVG Sprites

Like we’ve done many times in the past, as a good practice, we’ll store all SVGs as symbols in an SVG sprite container. Then, we’ll render them on the screen whenever we need by calling the use element.

Here’s the markup for the SVG sprite:

Notice the preserveAspectRatio="none" attribute which we attached to most of the illustrations. We’ve done this because, as we’ll see later, our icons will scale and lose their initial dimensions.


The container will include a form, a div element, and an empty ordered list:

Inside the form, we’ll have an input and a submit button along with their associated SVGs:

Notice the name attribute that we’ve added to the input field. Later we’ll use this attribute to access the input value after the form submission.

Note: In our demo, the autofocus attribute of the text field won’t work. In fact, it’ll throw the following error which you can see if you open your browser console:

The cross-origin error due to the autofocus attributeThe cross-origin error due to the autofocus attributeThe cross-origin error due to the autofocus attribute

However, if you run this app locally (not as a Codepen project), this issue won’t exist. Alternatively, you can set the focus via JavaScript.

Inside the div, we’ll place three nested divs and the associated SVG. In this section we’ll keep track of the total number of tasks (both remaining and completed):

Finally, the items of the ordered list will be added dynamically through JavaScript. 

3. Define Some Basic Styles

With the markup ready, we’ll continue with some reset styles:

4. Set the Main Styles

Let’s now discuss the main styles of our app.

Container Styles

The container will have a maximum width with horizontally centered content:

Form Styles

On small screens all form elements will be stacked:

The form layout on small screensThe form layout on small screensThe form layout on small screens

However, on viewports 600 pixels wide and above, the form layout will change as follows:

The form layout on medium screens and aboveThe form layout on medium screens and aboveThe form layout on medium screens and above

Let’s take note of two things:

  • On wide viewports, the input will be twice the size of the button. 
  • The SVGs will be absolutely positioned elements and sit below their adjacent form control. Again, for a more detailed explanation, have a look at this previous tutorial.

Here are the styles for this section:

Stats Styles

Next, let’s look at the status bar which will give us a quick report about the total number of tasks.

On small screens it will have the following stacked appearance:

The stats layout on small screensThe stats layout on small screensThe stats layout on small screens

However, on viewports 600 pixels wide and above, it should change as follows:

The stats layout on medium screens and aboveThe stats layout on medium screens and aboveThe stats layout on medium screens and above

Let’s take note of two things:

  • On wide viewports, all child div elements will have equal widths.
  • Similarly to the previous SVGs, this will also be absolutely positioned and act as a background image that covers the whole section.

The related styles:

Task Styles

The tasks layout, which we’ll generate dynamically in the upcoming section, will look like this:

The tasks layoutThe tasks layoutThe tasks layout

Each task which will be represented by a li will have two parts. 

The markup representation of a taskThe markup representation of a taskThe markup representation of a task

In the first part, you’ll see a checkbox along with the task name. In the second part, you’ll notice a delete button for removing the task.

Here are the related styles:

When a task is incomplete, an empty checkbox will appear. On the other hand, if a task is marked as completed, a checkmark will appear. Additionally, its name will be given 50% opacity as well as a line through it.

Here are the styles responsible for this behavior:

Finally, below are the styles for the delete button:

5. Add the JavaScript

At this point, we’re ready to build the core functionality of our app. Let’s do it!

On Form Submission

Each time a user submits the form by pressing the Enter key or the Submit button, we’ll do the following things:

  1. Stop the form from submitting, thereby preventing a reload of the page.
  2. Grab the value which is contained in the input field.
  3. Assuming that the input field isn’t empty, we’ll create a new object literal which will represent the task. Each task will have a unique id, a name, and be active (not completed) by default.
  4. Add this task to the tasks array.
  5. Store the array in local storage. Local storage only supports strings, so to do it, we have to use the JSON.stringify() method to convert the objects inside the array into strings. 
  6. Call the createTask() function for visually representing the task on the screen.
  7. Clear the form.
  8. Give focus to the input field.

Here’s the relevant code:

Create a Task

The createTask() function will be responsible for creating the task’s markup.

For instance, here’s the structure for the “Go for a walk” task:

The markup structure for a taskThe markup structure for a taskThe markup structure for a task

Two things are important here:

  • If the task is completed, the checkmark will appear.
  • If the task isn’t completed, its span element will receive the contenteditable attribute. This attribute will give us the ability to edit/update its name.

Below is the syntax for this function:

Update a Task

A task can be updated in two different ways:

  • By changing its status from “incomplete” to “completed” and vice versa.
  • By modifying its name in case the task is incomplete. Remember that in this case, the span element has the contenteditable attribute.

To keep track of these changes, we’ll take advantage of the input event. This is an acceptable event for us because it applies both to input elements and elements with contenteditable enabled.

The tricky thing is that we cannot directly attach this event to the target elements (checkbox, span) because they are created dynamically and aren’t part of the DOM on page load.

Thanks to the event delegation, we’ll attach the input event to the parent list which is part of the initial markup. Then, via the target property of that event we’ll check the elements on which the event occurred and call the updateTask() function:

Inside the updateTask() function, we’ll do the following things:

  1. Grab the task that needs to be updated.
  2. Check the element that triggered the event. If the element has the contenteditable attribute (i.e. it’s the span element), we’ll set the task’s name equal to the span’s text content.
  3. Otherwise (i.e. it’s the checkbox), we’ll toggle the task’s status and its checked attribute. Plus, we’ll also toggle the contenteditable attribute of the adjacent span.
  4. Update the value of the tasks key in local storage.
  5. Call the countTasks() function.

Here’s the syntax for this function:

Remove a Task

We can remove a task via the “close” button. 

Button for removing a taskButton for removing a taskButton for removing a task

Similar to the update operation, we cannot directly attach an event to this button because it isn’t in the DOM when the page loads.

Thanks again to the event delegation, we’ll attach a click event to the parent list and perform the following actions:

  1. Check if the element that is clicked is the “close” button or its child SVG.
    1. If that happens, we’ll grab the id of the parent list item.
    2. Pass this id to the removeTask() function.

    Here’s the relevant code:

    Inside the removeTask() function, we’ll do the following things:

    1. Remove from the tasks array the associated task.
    2. Update the value of the tasks key in local storage.
    3. Remove the associated list item.
    4. Call the countTasks() function.

    Here’s the syntax for this function:

    Count Tasks

    As we’ve already discussed, many of the functions above include the countTask() function.

    Its job is to monitor the tasks for changes (additions, updates, deletions) and update the content of the related elements.

    Count tasksCount tasksCount tasks

    Here’s its signature:

    Prevent Adding New Lines

    Each time a user updates the name of a task, they should not be able to create new lines by pressing the Enter key.

    Prevent multi linesPrevent multi linesPrevent multi lines

    To disable this functionality, once again we’ll take advantage of the event delegation and attach the keydown event to the list, like this:

    Note that in this scenario only the span elements could trigger that event, so there’s no need to make an additional check like this:

    Persist Data on Page Load

    So far, if we close the browser and navigate to the demo project, our tasks will disappear.

    But, wait that isn’t 100% true! Remember that each time we do a task manipulation, we also store the tasks array in local storage. For example, in Chrome, to see the local storage keys and values, click the Application tab then, expand the Local Storage menu and finally click a domain to view its key-value pairs.

    In my case, here are the values for the tasks key:

    An example with local storageAn example with local storageAn example with local storage

    So, to display these tasks, we first need to retrieve them from local storage. To do this, we’ll use the JSON.parse() method which will convert the strings back to JavaScript objects. 

    Next, we’ll store all tasks in the familiar tasks array. Keep in mind that if there’s no data in local storage (for instance the very first time we visit the app), this array is empty. Then, we have to iterate through the array, and for each task, call the createTask() function. And, that’s all!

    The corresponding code snippet:


    Phew! Thanks for following along on this long journey folks. Hopefully, you gained some new knowledge today which you’ll be able to apply to your own projects.

    Let’s remind ourselves what we built:

    Without a doubt, building such an app with a JavaScript framework might be more stable, easy, and efficient (repainting the DOM is expensive). However, knowing to solve this kind of exercise with plain JavaScript will help you get a solid grasp on its fundamentals and make you a better JavaScript developer.

    Before closing, let me propose two ideas for extending this exercise:

    • Use the HTML Drag and Drop API or a JavaScript library like Sortable.js for reordering the tasks.
    • Store data (tasks) in the cloud instead of the browser. For example, replace local storage with a real-time database like Firebase.

    As always, thanks a lot for reading!

    More Vanilla JavaScript Apps

    If you want to want to learn building small apps with plain JavaScript, check out the following tutorials:

    Did you find this post useful?
    Want a weekly email summary?
    Subscribe below and we’ll send you a weekly email summary of all new Web Design tutorials. Never miss out on learning about the next big thing.
    Looking for something to help kick start your next project?
    Envato Market has a range of items for sale to help get you started.