Create a PWA banner

How to Create a Progressive Web App

With so many bulky apps on the market, Progressive Web Apps (PWAs) offer a refreshingly lightweight alternative. Today, I'll show you how to transform a basic web page into a super-fast, offline-accessible tool that rivals native apps, without the clutter. In this tutorial you will learn how to create a PWA from scratch with HTML, CSS, and JavaScript. Our app will be a simple todo list that lets the user add, edit, and delete tasks.

What Is a Progressive Web App?

A PWA is a web application that combines the features of traditional websites (accessibility, discoverability) with the capabilities of native apps (offline functionality, install prompts, push notifications). Built using web technologies like HTML, CSS, and JavaScript, PWAs use service workers to enable offline caching and background sync, delivering a native-like experience.

Before we dive into the nitty-gritty, let's understand what makes a PWA special:

  1. Progressive Enhancement: PWAs work for everyone, regardless of the browser or device. They progressively enhance based on the capabilities of the user's environment.
  2. Responsive Design: PWAs adapt seamlessly to various screen sizes, from mobile phones to desktops.
  3. Offline Capabilities: Perhaps the most exciting feature! PWAs can function offline, thanks to service workers and caching strategies.
  4. App-Like Experience: PWAs feel like native apps, complete with smooth animations, push notifications, and home screen installation.

Some popular progressive web apps

PWAs aren't just trendy tech, they're used by industry giants like Pinterest (engagement up 60%!), Starbucks (orders up 30%), and Forbes (page views up 40%). Forget clunky downloads, these websites zoom past offline dead zones and feel just like native apps. Think of Spotify offline jams, AliExpress on the go, or Twitter Lite conquering slow internet - all with your website.

Now that we know what a PWA is, let's start bulding our own:

Step 1: Create the HTML file

The HTML file is the main entry point of your web app. It contains the basic structure and content of your app, such as the title, the header, the input field, and the list of tasks. You also need to link the CSS and JavaScript files that you will create later. Here is the code for the HTML file:

index.html
                      
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <meta http-equiv="X-UA-Compatible" content="ie=edge" />
  <link rel="stylesheet" href="style.css" />
  <title>Todo PWA</title>
</head>
<body>
  <header>
    <h1>Todo PWA</h1>
  </header>
  <main>
    <form id="task-form">
      <input type="text" id="task-input" placeholder="Enter a task" required />
      <button type="submit" id="task-submit">Add</button>
    </form>
    <ul id="task-list"></ul>
  </main>
  <script src="app.js"></script>
</body>
</html>

If you open that file in your browser, you will see something like the image below. Pretty plain, right? Well let's add some CSS to make it a bit nicer


To-Do PWA without CSS styling

Step 2: Create the CSS file

The CSS file is used to style the HTML elements and make them look more appealing. You can use any colors, fonts, and layouts that you like. For this tutorial, create a file and name it style.css and add the following code:

style.css
                      
* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}

body {
  background: #f0f0f0;
  font-family: Arial, sans-serif;
  font-size: 16px;
}

header {
  background: #3f51b5;
  color: white;
  padding: 1rem;
  text-align: center;
}

main {
  max-width: 600px;
  margin: 1rem auto;
  padding: 1rem;
}

form {
  display: flex;
  align-items: center;
}

input {
  flex: 1;
  border: none;
  outline: none;
  padding: 0.5rem;
  border-radius: 0.25rem;
}

button {
  margin-left: 0.5rem;
  border: none;
  outline: none;
  padding: 0.5rem;
  background: #3f51b5;
  color: white;
  border-radius: 0.25rem;
  cursor: pointer;
}

ul {
  list-style: none;
  margin-top: 1rem;
}

li {
  display: flex;
  align-items: center;
  background: white;
  padding: 0.5rem;
  margin-bottom: 0.5rem;
  border-radius: 0.25rem;
}

li.done {
  text-decoration: line-through;
  color: gray;
}

li span {
  flex: 1;
}

li button {
  margin-left: 0.5rem;
  border: none;
  outline: none;
  padding: 0.5rem;
  background: #e74c3c;
  color: white;
  border-radius: 0.25rem;
  cursor: pointer;
}

After adding the css, if you refresh your browser, you should see something like this:


To-Do PWA with CSS styling

Step 3: Create the JavaScript file

The JavaScript file is used to add interactivity and functionality to your web app. It handles the user input, the data manipulation, and the DOM manipulation. You need to declare some variables to store the HTML elements, the array of tasks, and the localStorage key. You also need to add some event listeners to handle the form submission, the task completion, and the task deletion. To achieve that, create a JavaScript file and add the following code:

app.js
                      
// Get the HTML elements
const taskForm = document.getElementById("task-form");
const taskInput = document.getElementById("task-input");
const taskList = document.getElementById("task-list");

// Define the array of tasks and the localStorage key
let tasks = [];
const TASKS_KEY = "tasks";

// Load the tasks from the localStorage
function loadTasks() {
  // Get the tasks from the localStorage as a JSON string
  const tasksJSON = localStorage.getItem(TASKS_KEY);

  // Parse the JSON string to an array
  if (tasksJSON) {
    tasks = JSON.parse(tasksJSON);
  }

  // Render the tasks to the DOM
  renderTasks();
}

// Save the tasks to the localStorage
function saveTasks() {
  // Stringify the array to a JSON string
  const tasksJSON = JSON.stringify(tasks);

  // Set the JSON string to the localStorage
  localStorage.setItem(TASKS_KEY, tasksJSON);
}

// Render the tasks to the DOM
function renderTasks() {
  // Clear the task list
  taskList.innerHTML = "";

  // Loop through the tasks array
  for (let i = 0; i < tasks.length; i++) {
  // Get the current task object
    const task = tasks[i];
  
    // Create a new li element
    const li = document.createElement("li");

    // Add the done class if the task is completed
    if (task.completed) {
      li.classList.add("done");
    }

    // Create a new span element to hold the task text
    const span = document.createElement("span");
    span.textContent = task.text;

    // Create a new button element to delete the task
    const button = document.createElement("button");
    button.textContent = "Delete";

    // Add an event listener to toggle the task completion
    span.addEventListener("click", function () {
      // Toggle the task completed property
      task.completed = !task.completed;

      // Save the tasks
      saveTasks();

      // Render the tasks
      renderTasks();
    });

    // Add an event listener to delete the task
    button.addEventListener("click", function () {
      // Remove the task from the tasks array
      tasks.splice(i, 1);

      // Save the tasks
      saveTasks();

      // Render the tasks
      renderTasks();
    });

    // Append the span and the button to the li element
    li.appendChild(span);
    li.appendChild(button);

    // Append the li element to the task list
    taskList.appendChild(li);
  }
}

// Add an event listener to handle the form submission
taskForm.addEventListener("submit", function (e) {
  // Prevent the default behavior of the form
  e.preventDefault();

  // Get the task input value
  const taskText = taskInput.value;

  // Validate the input
  if (taskText.trim() === "") {
    alert("Please enter a task");
    return;
  }

  // Create a new task object
  const task = {
    text: taskText,
    completed: false,
  };

  // Add the task to the tasks array
  tasks.push(task);

  // Save the tasks
  saveTasks();

  // Render the tasks
  renderTasks();

  // Clear the task input value
  taskInput.value = "";
});

// Load the tasks when the page loads
loadTasks();

Now if we refresh our browser and add stuff to our to do-list it will look like this:


To-Do List PWA with some things to do

Step 4: Create the web app manifest file

The web app manifest file is a JSON file that describes your web app to the host operating system. It contains information such as the name, the icon, the theme color, the display mode, and the start URL of your web app. You need to create a manifest.json file in the same folder as your HTML file and add the following code:

manifest.json
                      
{
  "name": "Todo PWA",
  "short_name": "Todo",
  "icons": [
    {
      "src": "icon-192.png",
      "sizes": "192x192",
      "type": "image/png"
    },
    {
      "src": "icon-512.png",
      "sizes": "512x512",
      "type": "image/png"
    }
  ],
  "theme_color": "#3f51b5",
  "background_color": "#f0f0f0",
  "display": "standalone",
  "start_url": "/index.html"
}

You also need to add two icon images with the sizes of 192 x 192 and 512 x 512 pixels in the same folder as your manifest file. You can use any image editor to create these icons, or you can use a tool like RealFaviconGenerator to generate them automatically.

To link the manifest file to your HTML file, you need to add the following tag in the head section of your HTML file:

HTML
                      
<link rel="manifest" href="manifest.json" />
                      
                  

Step 5: Create the service worker file

The service worker file is a JavaScript file that runs in the background and acts as a proxy between your web app and the network. It can intercept requests, cache responses, and handle offline scenarios. You need to create a service-worker.js file in the same folder as your HTML file and add the following code:

service-worker.js
                      
// Define the cache name and the files to cache
const cacheName = "todo-pwa-v1";
const filesToCache = [
  "/",
  "/index.html",
  "/css/style.css",
  "/js/app.js",
  "/icon-192.png",
  "/icon-512.png",
];

// Install the service worker and cache the files
self.addEventListener("install", function (e) {
  e.waitUntil(
    caches.open(cacheName).then(function (cache) {
    return cache.addAll(filesToCache);
    })
  );
});

// Activate the service worker and delete the old caches
self.addEventListener("activate", function (e) {
  e.waitUntil(
    caches.keys().then(function (keyList) {
      return Promise.all(
        keyList.map(function (key) {
          if (key !== cacheName) {
            return caches.delete(key);
          }
        })
      );
    })
  );
});

// Fetch the files from the cache or the network
self.addEventListener("fetch", function (e) {
  e.respondWith(
    caches.match(e.request).then(function (response) {
      return response || fetch(e.request);
    })
  );
});

Step 6: Link the service worker file to your HTML file

To register the service worker file to your web app, you need to add the following script tag in the head section of your HTML file:

Javascript
                      
<script>
    // Check if the browser supports service workers
    if ("serviceWorker" in navigator) {
        window.addEventListener("load", () => {
            // Register the service worker file
            navigator.serviceWorker
                .register("service-worker.js")
                .then(function () {
                    console.log("Service worker registered");
                })
                .catch(function (error) {
                    console.log("Service worker registration failed", error);
                });
        });
    }
</script>

Step 7: Test your PWA

To test your PWA, you need to run it on a local web server. You can use any web server of your choice.


icon showing option to download PWA in browser

If you click the icon, your browser will show you a prompt similar to the one below. If you click install, your PWA will be installed on the device.


Prompt confirming whether to install pwa

To test the offline functionality of your PWA, you can use the Chrome DevTools. Open the DevTools by pressing F12 or Ctrl+Shift+I, then go to the Application tab and select Service Workers from the left panel. There, you can check the Offline option to simulate the offline mode. You can also use the Network tab and select Offline from the dropdown menu.

To test the installation prompt of your PWA, you can use the Chrome DevTools as well. Go to the Application tab and select Manifest from the left panel. There, you can click on the Add to home screen button to trigger the installation prompt. You can also use the Lighthouse tool to audit your PWA and check if it meets the criteria for installation.

Troubleshooting

If you find yourself facing some challenges and not getting the desired result, you can try the following

  • Ensure that you linked correctly to your service-worker and manifest.json files. If they are in a different directory, use the correct url.
  • Check and make sure that all the assets and resources listed in your manifest.json file such as icons, exist and are in the stated location.
  • Make sure the URLs in your serviceWorker of the files you are caching are all correct
  • Check the scope of all your files to ensure they are accessible to your PWA

Step 8: Deploy your PWA

To deploy your PWA, you need to upload your files to a web server that supports HTTPS. You can use any hosting service of your choice. For starters, you can use a platform like Netlify, which is a free and easy way to deploy web apps.

That's it! You have successfully created and deployed a PWA from scratch with HTML, CSS, and JavaScript. You can now access your PWA from any device and enjoy its app-like features.