Next.js and Progressive Web Apps (PWAs): A Beginner’s Guide

In today’s fast-paced digital world, users expect websites to be fast, reliable, and accessible, regardless of their internet connection. This is where Progressive Web Apps (PWAs) come into play. PWAs offer a way to deliver app-like experiences directly through the web, providing features like offline access, push notifications, and improved performance. Next.js, a powerful React framework, provides excellent support for building PWAs, making the development process smoother and more efficient. This tutorial will guide you through the process of creating a PWA with Next.js, covering the essential concepts and practical implementation steps.

Understanding Progressive Web Apps (PWAs)

Before diving into the code, let’s understand what makes a PWA. Essentially, a PWA is a web application that uses modern web capabilities to deliver an app-like experience to users. Here are some key characteristics:

  • Progressive: Works for every user, regardless of browser choice because it’s built with progressive enhancement as a core tenet.
  • Responsive: Fits any form factor: desktop, mobile, tablet, or whatever is next.
  • Connectivity independent: Enhanced with service workers to work offline or on low-quality networks.
  • App-like: Feels like an app to the user with app-shell, manifest file, and add to home screen features.
  • Fresh: Always up-to-date thanks to the service worker update process.
  • Safe: Served via HTTPS to prevent snooping and ensure content hasn’t been tampered with.
  • Discoverable: Is identifiable as an “application” thanks to W3C manifests and service worker registration scope, allowing search engines to find it.
  • Re-engageable: Makes re-engagement easy through features like push notifications.
  • Installable: Allows users to “install” the app on their home screen, providing quicker access.
  • Linkable: Easily shared via a URL and doesn’t require complex installation.

PWAs leverage technologies like service workers, web app manifests, and HTTPS to achieve these features. Service workers enable offline capabilities, push notifications, and background syncing, while the web app manifest provides information about the app, such as its name, icons, and theme color, allowing users to install it on their home screens.

Setting Up Your Next.js Project

Let’s start by creating a new Next.js project. If you don’t have Node.js and npm (or yarn) installed, you’ll need to install them first. Once you have those, open your terminal and run the following command:

npx create-next-app pwa-tutorial

This command creates a new Next.js project named “pwa-tutorial.” Navigate into the project directory:

cd pwa-tutorial

Now, install the necessary dependencies for PWA support. We’ll use the ‘next-pwa’ package, which simplifies the process of creating a PWA in Next.js.

npm install next-pwa --save-dev

Configuring Next.js for PWA

Next, we need to configure our Next.js application to use the ‘next-pwa’ package. Open your `next.config.js` file and add the following code. If you don’t have a `next.config.js` file, create one in the root of your project.

const withPWA = require('next-pwa')

module.exports = withPWA({
  pwa: {
    dest: 'public',
    register: true,
    skipWaiting: true,
  },
})

Let’s break down these configurations:

  • `withPWA`: This is a function provided by the `next-pwa` package that wraps our Next.js configuration.
  • `pwa`: This object contains the PWA-specific configurations.
  • `dest: ‘public’`: Specifies the directory where the service worker and other PWA files will be generated. The `public` directory is the default location for static assets in Next.js.
  • `register: true`: Enables the service worker registration.
  • `skipWaiting: true`: Instructs the service worker to activate immediately after installation, bypassing the waiting state. This ensures that the latest version of the app is available as soon as possible.

Creating the Web App Manifest

The web app manifest is a JSON file that provides information about your PWA, such as its name, icons, and theme color. This information is used by the browser to display the app in the user’s home screen and provide a more native app-like experience.

Create a file named `manifest.json` in the `public` directory of your project (i.e., `public/manifest.json`) and add the following content. Customize the values to match your application.

{
  "name": "My PWA App",
  "short_name": "MyPWA",
  "description": "A simple PWA built with Next.js",
  "start_url": "/",
  "display": "standalone",
  "background_color": "#ffffff",
  "theme_color": "#000000",
  "icons": [
    {
      "src": "/icon-192x192.png",
      "sizes": "192x192",
      "type": "image/png"
    },
    {
      "src": "/icon-512x512.png",
      "sizes": "512x512",
      "type": "image/png"
    }
  ]
}

Here’s what each field means:

  • `name`: The full name of your app.
  • `short_name`: A shorter version of your app’s name, used when space is limited.
  • `description`: A brief description of your app.
  • `start_url`: The URL that loads when the app is launched.
  • `display`: How the app should be displayed. “standalone” creates an app-like experience without a browser UI. Other options include “fullscreen”, “minimal-ui”, and “browser”.
  • `background_color`: The background color of the app’s splash screen.
  • `theme_color`: The theme color for the app’s UI elements, such as the status bar.
  • `icons`: An array of icon objects, specifying the app’s icons for different screen sizes. You’ll need to provide at least two icons: one for smaller screens (e.g., 192×192) and one for larger screens (e.g., 512×512). Create these icons and place them in your `public` directory.

Adding the Manifest to Your Pages

To make the manifest accessible to the browser, you need to add a link tag to your HTML “. Next.js provides a built-in “ component for this purpose. Open your `pages/_document.js` file and modify it to include the following. If you don’t have a `_document.js` file, create one in the `pages` directory.

import Document, { Html, Head, Main, NextScript } from 'next/document'

class MyDocument extends Document {
  render() {
    return (
      
        
          
        
        
          <Main />
          
        
      
    )
  }
}

export default MyDocument

This code imports the necessary components from Next.js and adds a “ tag to the “ that specifies the location of your `manifest.json` file. This allows the browser to recognize your application as a PWA and prompt the user to install it.

Adding Icons

As mentioned in the manifest section, you’ll need to create icons for your PWA. These icons should be in PNG format and come in different sizes to accommodate various screen resolutions. Create the following icons and place them in your `public` directory:

  • `icon-192×192.png`
  • `icon-512×512.png`

You can use online icon generators or design tools like Adobe Photoshop or GIMP to create these icons. Ensure they are visually appealing and represent your application’s brand.

Implementing Offline Support with Service Workers

The core of a PWA’s offline capabilities is the service worker. The `next-pwa` package automatically generates and configures a service worker for you, making it easy to implement offline support. The service worker intercepts network requests and caches the necessary assets, allowing your application to function even when the user is offline.

After you’ve configured `next.config.js` and added the manifest, rebuild your application:

npm run build

Then, run your application locally:

npm run dev

Now, when you inspect your application in the browser’s developer tools (Application tab -> Service Workers), you should see a service worker registered. This service worker is responsible for caching your application’s assets and enabling offline functionality.

Testing Your PWA

To verify that your PWA is working correctly, you can perform the following tests:

  1. Install the App: If you’ve followed all the steps correctly, the browser should prompt you to install the PWA on your device. Click the install button and test the installed app.
  2. Offline Mode: Enable offline mode in your browser’s developer tools (Network tab) or disconnect your internet connection. Navigate to your PWA and ensure that it still functions and loads the cached content.
  3. Inspect the Service Worker: In the developer tools (Application tab -> Service Workers), you can see the service worker’s status, cached assets, and other details.
  4. Check the Lighthouse Report: Use Lighthouse (in Chrome DevTools) to audit your PWA. Lighthouse will provide a score and detailed recommendations for improving your PWA’s performance, accessibility, and best practices. Look for high scores in the PWA category.

Common Mistakes and Troubleshooting

Here are some common mistakes and how to fix them:

  • Incorrect `next.config.js` Configuration: Double-check your `next.config.js` file to ensure the `next-pwa` configuration is correct. Ensure that the `dest` property points to the `public` directory and that `register` is set to `true`.
  • Missing or Incorrect Manifest File: Verify that your `manifest.json` file is correctly formatted and placed in the `public` directory. Check the file paths in your “ tag in `_document.js`.
  • Incorrect Icon Sizes: Make sure you have provided icons in the required sizes (e.g., 192×192, 512×512) and that the paths in your `manifest.json` file are correct.
  • Service Worker Not Registering: If the service worker is not registering, check the browser’s console for any errors. Also, ensure that your application is served over HTTPS in production.
  • Caching Issues: Sometimes, the browser might cache the old version of your application. Try clearing your browser’s cache or using a hard reload (Ctrl+Shift+R or Cmd+Shift+R) to ensure that the latest version is loaded.
  • Incorrect Deployment: When deploying your app, ensure that your hosting platform supports service workers and serves the necessary files correctly. Vercel, for example, handles this automatically.

SEO Considerations for PWAs

While PWAs are primarily focused on user experience, it’s essential to consider SEO best practices to ensure your application is discoverable by search engines.

  • Use Meaningful Titles and Descriptions: Ensure your `` and “ tags in your HTML “ accurately describe your application.</li> <li><b>Provide Descriptive URLs:</b> Use clear and concise URLs for your pages.</li> <li><b>Optimize Content for Search Engines:</b> Use relevant keywords and phrases in your content.</li> <li><b>Ensure Mobile-Friendliness:</b> PWAs are inherently mobile-friendly, but ensure your content is responsive and accessible on all devices.</li> <li><b>Submit Your Sitemap:</b> Submit your sitemap to search engines to help them crawl and index your content.</li> <li><b>Use Structured Data:</b> Implement structured data (schema.org) markup to provide search engines with more information about your content.</li> </ul> <h2>Key Takeaways</h2> <ul> <li>Next.js provides excellent support for building Progressive Web Apps.</li> <li>The `next-pwa` package simplifies the PWA setup process.</li> <li>The web app manifest defines your app’s metadata.</li> <li>Service workers enable offline functionality and background syncing.</li> <li>Test your PWA thoroughly to ensure it functions correctly.</li> <li>Optimize your PWA for SEO to improve its discoverability.</li> </ul> <h2>FAQ</h2> <ol> <li><b>What are the benefits of using a PWA?</b> <p>PWAs offer numerous benefits, including improved performance, offline access, push notifications, app-like installation, and better user engagement. They can lead to higher user retention and conversion rates.</p> </li> <li><b>Do I need a server to host a PWA?</b> <p>No, you don’t necessarily need a dedicated server to host a PWA. PWAs can be hosted on various platforms, including static site hosting services like Netlify or Vercel. These services often provide features optimized for serving PWAs.</p> </li> <li><b>Can I use PWAs with existing websites?</b> <p>Yes, you can progressively enhance existing websites to become PWAs. You can add a service worker, a manifest file, and other PWA features without completely rebuilding your website.</p> </li> <li><b>Are PWAs supported on all browsers?</b> <p>PWAs are supported by all modern browsers, including Chrome, Firefox, Safari, and Edge. However, the level of support may vary slightly depending on the browser and operating system. Most features are widely supported.</p> </li> <li><b>How do I update my PWA?</b> <p>When you update your PWA, the service worker will automatically update the cached assets. Users typically need to refresh the app or close and reopen it to see the latest version. You can also use strategies like “cache-busting” to force updates by changing the filenames of your assets.</p> </li> </ol> <p>By following these steps, you can create a fully functional PWA with Next.js, providing a fast, reliable, and engaging experience for your users. Remember to test your PWA thoroughly and optimize it for SEO to maximize its impact. The development of PWAs continues to evolve, offering new possibilities for web developers to create compelling and user-friendly applications. Building a PWA with Next.js opens up exciting opportunities to reach a wider audience and deliver a superior web experience. As the web evolves, PWAs will continue to be a crucial part of the modern web development landscape, offering a bridge between the traditional web and the world of native mobile applications. Embrace the power of PWAs and create web applications that are not only functional but also delightful to use. The future of web development is undoubtedly intertwined with the capabilities of PWAs, and by mastering the skills outlined in this guide, you’ll be well-equipped to navigate this exciting technological frontier.</p> <div class="wpzoom-social-sharing-buttons-bottom"><div class="wp-block-wpzoom-blocks-social-sharing align-none"><ul class="social-sharing-icons"><li class="social-sharing-icon-li"><a class="social-sharing-icon social-sharing-icon-facebook" style="padding:5px 15px;margin:5px 5px;border-radius:50px;font-size:20px;color:#ffffff;background-color:#0866FF;" href="https://www.facebook.com/sharer/sharer.php?u=https%3A%2F%2Flearnmodernjavascript.com%2Fnext-js-and-progressive-web-apps-pwas-a-beginners-guide%2F&t=Next.js+and+Progressive+Web+Apps+%28PWAs%29%3A+A+Beginner%26%238217%3Bs+Guide" title="Facebook" target="_blank" rel="noopener noreferrer" data-platform="facebook"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="20" height="20" style="fill:#ffffff;" aria-hidden="true" focusable="false"><path d="M24 12.073c0-6.627-5.373-12-12-12s-12 5.373-12 12c0 5.99 4.388 10.954 10.125 11.854v-8.385H7.078v-3.47h3.047V9.43c0-3.007 1.792-4.669 4.533-4.669 1.312 0 2.686.235 2.686.235v2.953H15.83c-1.491 0-1.956.925-1.956 1.874v2.25h3.328l-.532 3.47h-2.796v8.385C19.612 23.027 24 18.062 24 12.073z" /></svg><span class="social-sharing-icon-label" style="font-size:16px;color:inherit;">Facebook</span></a></li><li class="social-sharing-icon-li"><a class="social-sharing-icon social-sharing-icon-x" style="padding:5px 15px;margin:5px 5px;border-radius:50px;font-size:20px;color:#ffffff;background-color:#000000;" href="https://x.com/intent/tweet?url=https%3A%2F%2Flearnmodernjavascript.com%2Fnext-js-and-progressive-web-apps-pwas-a-beginners-guide%2F&text=Next.js+and+Progressive+Web+Apps+%28PWAs%29%3A+A+Beginner%26%238217%3Bs+Guide" title="Share on X" target="_blank" rel="noopener noreferrer" data-platform="x"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="20" height="20" style="fill:#ffffff;" aria-hidden="true" focusable="false"><path d="M18.901 1.153h3.68l-8.04 9.19L24 22.846h-7.406l-5.8-7.584-6.638 7.584H.474l8.6-9.83L0 1.154h7.594l5.243 6.932ZM17.61 20.644h2.039L6.486 3.24H4.298Z" /></svg><span class="social-sharing-icon-label" style="font-size:16px;color:inherit;">Share on X</span></a></li><li class="social-sharing-icon-li"><a class="social-sharing-icon social-sharing-icon-linkedin" style="padding:5px 15px;margin:5px 5px;border-radius:50px;font-size:20px;color:#ffffff;background-color:#0966c2;" href="https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Flearnmodernjavascript.com%2Fnext-js-and-progressive-web-apps-pwas-a-beginners-guide%2F" title="LinkedIn" target="_blank" rel="noopener noreferrer" data-platform="linkedin"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="20" height="20" style="fill:#ffffff;" aria-hidden="true" focusable="false"><path d="M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433c-1.144 0-2.063-.926-2.063-2.065 0-1.138.92-2.063 2.063-2.063 1.14 0 2.064.925 2.064 2.063 0 1.139-.925 2.065-2.064 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003z" /></svg><span class="social-sharing-icon-label" style="font-size:16px;color:inherit;">LinkedIn</span></a></li><li class="social-sharing-icon-li"><a class="social-sharing-icon social-sharing-icon-whatsapp" style="padding:5px 15px;margin:5px 5px;border-radius:50px;font-size:20px;color:#ffffff;background-color:#25D366;" href="https://api.whatsapp.com/send?text=Next.js+and+Progressive+Web+Apps+%28PWAs%29%3A+A+Beginner%26%238217%3Bs+Guide+https%3A%2F%2Flearnmodernjavascript.com%2Fnext-js-and-progressive-web-apps-pwas-a-beginners-guide%2F" title="WhatsApp" target="_blank" rel="noopener noreferrer" data-platform="whatsapp"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="20" height="20" style="fill:#ffffff;" aria-hidden="true" focusable="false"><path d="M17.472 14.382c-.297-.149-1.758-.867-2.03-.967-.273-.099-.471-.148-.67.15-.197.297-.767.966-.94 1.164-.173.199-.347.223-.644.075-.297-.15-1.255-.463-2.39-1.475-.883-.788-1.48-1.761-1.653-2.059-.173-.297-.018-.458.13-.606.134-.133.298-.347.446-.52.149-.174.198-.298.298-.497.099-.198.05-.371-.025-.52-.075-.149-.669-1.612-.916-2.207-.242-.579-.487-.5-.669-.51-.173-.008-.371-.01-.57-.01-.198 0-.52.074-.792.372-.272.297-1.04 1.016-1.04 2.479 0 1.462 1.065 2.875 1.213 3.074.149.198 2.096 3.2 5.077 4.487.709.306 1.262.489 1.694.625.712.227 1.36.195 1.871.118.571-.085 1.758-.719 2.006-1.413.248-.694.248-1.289.173-1.413-.074-.124-.272-.198-.57-.347m-5.421 7.403h-.004a9.87 9.87 0 01-5.031-1.378l-.361-.214-3.741.982.998-3.648-.235-.374a9.86 9.86 0 01-1.51-5.26c.001-5.45 4.436-9.884 9.888-9.884 2.64 0 5.122 1.03 6.988 2.898a9.825 9.825 0 012.893 6.994c-.003 5.45-4.437 9.884-9.885 9.884m8.413-18.297A11.815 11.815 0 0012.05 0C5.495 0 .16 5.335.157 11.892c0 2.096.547 4.142 1.588 5.945L.057 24l6.305-1.654a11.882 11.882 0 005.683 1.448h.005c6.554 0 11.89-5.335 11.893-11.893a11.821 11.821 0 00-3.48-8.413z" /></svg><span class="social-sharing-icon-label" style="font-size:16px;color:inherit;">WhatsApp</span></a></li><li class="social-sharing-icon-li"><a class="social-sharing-icon social-sharing-icon-email" style="padding:5px 15px;margin:5px 5px;border-radius:50px;font-size:20px;color:#ffffff;background-color:#333333;" href="mailto:?subject=Next.js+and+Progressive+Web+Apps+%28PWAs%29%3A+A+Beginner%26%238217%3Bs+Guide&body=https%3A%2F%2Flearnmodernjavascript.com%2Fnext-js-and-progressive-web-apps-pwas-a-beginners-guide%2F" title="Email" target="_blank" rel="noopener noreferrer" data-platform="email"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="20" height="20" style="fill:#ffffff;" aria-hidden="true" focusable="false"><path d="M20 4H4c-1.1 0-1.99.9-1.99 2L2 18c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V6c0-1.1-.9-2-2-2zm-.4 4.25l-7.07 4.42c-.32.2-.74.2-1.06 0L4.4 8.25c-.25-.16-.4-.43-.4-.72 0-.67.73-1.07 1.3-.72L12 11l6.7-4.19c.57-.35 1.3.05 1.3.72 0 .29-.15.56-.4.72z" /></svg><span class="social-sharing-icon-label" style="font-size:16px;color:inherit;">Email</span></a></li><li class="social-sharing-icon-li"><a class="social-sharing-icon social-sharing-icon-copy-link" style="padding:5px 15px;margin:5px 5px;border-radius:50px;font-size:20px;color:#ffffff;background-color:#333333;" href="#copy-link" title="Copy Link" data-platform="copy-link"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="20" height="20" style="fill:#ffffff;" aria-hidden="true" focusable="false"><path d="M3.9 12c0-1.71 1.39-3.1 3.1-3.1h4V7H7c-2.76 0-5 2.24-5 5s2.24 5 5 5h4v-1.9H7c-1.71 0-3.1-1.39-3.1-3.1zM8 13h8v-2H8v2zm9-6h-4v1.9h4c1.71 0 3.1 1.39 3.1 3.1s-1.39 3.1-3.1 3.1h-4V17h4c2.76 0 5-2.24 5-5s-2.24-5-5-5z" /></svg><span class="social-sharing-icon-label" style="font-size:16px;color:inherit;">Copy Link</span></a></li></ul><script> document.addEventListener("DOMContentLoaded", function() { var copyLinks = document.querySelectorAll("a[data-platform='copy-link']"); copyLinks.forEach(function(link) { if (link.hasAttribute("data-listener-added")) return; link.setAttribute("data-listener-added", "true"); link.addEventListener("click", function(e) { e.preventDefault(); var tempInput = document.createElement("input"); tempInput.value = window.location.href; document.body.appendChild(tempInput); tempInput.select(); document.execCommand("copy"); document.body.removeChild(tempInput); var originalText = this.querySelector(".social-sharing-icon-label")?.textContent || ""; var originalTitle = this.getAttribute("title"); var originalIcon = this.querySelector("svg").outerHTML; // Show success feedback this.setAttribute("title", "Copied!"); this.classList.add("copied"); if (this.querySelector(".social-sharing-icon-label")) { this.querySelector(".social-sharing-icon-label").textContent = "Copied!"; } else { this.querySelector("svg").outerHTML = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="20" height="20" style="fill:#ffffff;" aria-hidden="true" focusable="false"><path d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41L9 16.17z" /></svg>'; } var self = this; setTimeout(function() { self.setAttribute("title", originalTitle); self.classList.remove("copied"); if (self.querySelector(".social-sharing-icon-label")) { self.querySelector(".social-sharing-icon-label").textContent = originalText; } else { self.querySelector("svg").outerHTML = originalIcon; } }, 2000); }); }); }); </script></div></div></div> <div class="wp-block-group has-global-padding is-layout-constrained wp-block-group-is-layout-constrained" style="padding-top:var(--wp--preset--spacing--60);padding-bottom:var(--wp--preset--spacing--60)"><div class="taxonomy-post_tag is-style-post-terms-1 is-style-post-terms-1--2 wp-block-post-terms"><a href="https://learnmodernjavascript.com/tag/javascript/" rel="tag">JavaScript</a><span class="wp-block-post-terms__separator"> </span><a href="https://learnmodernjavascript.com/tag/next-js/" rel="tag">Next.js</a><span class="wp-block-post-terms__separator"> </span><a href="https://learnmodernjavascript.com/tag/offline/" rel="tag">Offline</a><span class="wp-block-post-terms__separator"> </span><a href="https://learnmodernjavascript.com/tag/progressive-web-app/" rel="tag">Progressive Web App</a><span class="wp-block-post-terms__separator"> </span><a href="https://learnmodernjavascript.com/tag/pwa/" rel="tag">PWA</a><span class="wp-block-post-terms__separator"> </span><a href="https://learnmodernjavascript.com/tag/react/" rel="tag">React</a><span class="wp-block-post-terms__separator"> </span><a href="https://learnmodernjavascript.com/tag/service-worker/" rel="tag">Service Worker</a><span class="wp-block-post-terms__separator"> </span><a href="https://learnmodernjavascript.com/tag/tutorial/" rel="tag">Tutorial</a><span class="wp-block-post-terms__separator"> </span><a href="https://learnmodernjavascript.com/tag/web-app-manifest/" rel="tag">Web App Manifest</a><span class="wp-block-post-terms__separator"> </span><a href="https://learnmodernjavascript.com/tag/web-development/" rel="tag">Web Development</a></div></div> <div class="wp-block-group alignwide is-layout-flow wp-block-group-is-layout-flow" style="margin-top:var(--wp--preset--spacing--60);margin-bottom:var(--wp--preset--spacing--60)"> <nav class="wp-block-group alignwide is-content-justification-space-between is-nowrap is-layout-flex wp-container-core-group-is-layout-878fe601 wp-block-group-is-layout-flex" aria-label="Post navigation" style="border-top-color:var(--wp--preset--color--accent-6);border-top-width:1px;padding-top:var(--wp--preset--spacing--40);padding-bottom:var(--wp--preset--spacing--40)"><div class="post-navigation-link-previous wp-block-post-navigation-link"><span class="wp-block-post-navigation-link__arrow-previous is-arrow-arrow" aria-hidden="true">←</span><a href="https://learnmodernjavascript.com/next-js-and-animations-a-beginners-guide-to-engaging-user-interfaces/" rel="prev">Next.js and Animations: A Beginner’s Guide to Engaging User Interfaces</a></div> <div class="post-navigation-link-next wp-block-post-navigation-link"><a href="https://learnmodernjavascript.com/next-js-and-seo-a-beginners-guide-to-optimizing-your-website/" rel="next">Next.js and SEO: A Beginner’s Guide to Optimizing Your Website</a><span class="wp-block-post-navigation-link__arrow-next is-arrow-arrow" aria-hidden="true">→</span></div></nav> </div> </div> <div class="wp-block-group alignwide has-global-padding is-layout-constrained wp-block-group-is-layout-constrained" style="padding-top:var(--wp--preset--spacing--60);padding-bottom:var(--wp--preset--spacing--60)"> <h2 class="wp-block-heading alignwide has-small-font-size" style="font-style:normal;font-weight:700;letter-spacing:1.4px;text-transform:uppercase">More posts</h2> <div class="wp-block-query alignwide is-layout-flow wp-block-query-is-layout-flow"><ul class="alignfull wp-block-post-template is-layout-flow wp-container-core-post-template-is-layout-b4d04ffe wp-block-post-template-is-layout-flow"><li class="wp-block-post post-4507 post type-post status-publish format-standard hentry category-javascript tag-beginner tag-canvas tag-css tag-dom-manipulation tag-drawing-app tag-event-handling tag-html tag-interactive tag-tutorial tag-typescript tag-web-development"> <div class="wp-block-group alignfull is-content-justification-space-between is-nowrap is-layout-flex wp-container-core-group-is-layout-cba70755 wp-block-group-is-layout-flex" style="border-bottom-color:var(--wp--preset--color--accent-6);border-bottom-width:1px;padding-top:var(--wp--preset--spacing--30);padding-bottom:var(--wp--preset--spacing--30)"><h3 class="wp-block-post-title has-large-font-size"><a href="https://learnmodernjavascript.com/typescript-tutorial-building-a-simple-interactive-drawing-application/" target="_self" >TypeScript Tutorial: Building a Simple Interactive Drawing Application</a></h3> <div class="has-text-align-right wp-block-post-date"><a href="https://learnmodernjavascript.com/typescript-tutorial-building-a-simple-interactive-drawing-application/"><time datetime="2026-03-20T21:51:29+00:00">March 20, 2026</time></a></div></div> </li><li class="wp-block-post post-4451 post type-post status-publish format-standard hentry category-javascript tag-beginners tag-css tag-form-validation tag-html tag-intermediate tag-javascript tag-tutorial tag-typescript tag-web-development"> <div class="wp-block-group alignfull is-content-justification-space-between is-nowrap is-layout-flex wp-container-core-group-is-layout-cba70755 wp-block-group-is-layout-flex" style="border-bottom-color:var(--wp--preset--color--accent-6);border-bottom-width:1px;padding-top:var(--wp--preset--spacing--30);padding-bottom:var(--wp--preset--spacing--30)"><h3 class="wp-block-post-title has-large-font-size"><a href="https://learnmodernjavascript.com/typescript-tutorial-building-a-simple-interactive-form-with-validation/" target="_self" >TypeScript Tutorial: Building a Simple Interactive Form with Validation</a></h3> <div class="has-text-align-right wp-block-post-date"><a href="https://learnmodernjavascript.com/typescript-tutorial-building-a-simple-interactive-form-with-validation/"><time datetime="2026-03-20T18:01:15+00:00">March 20, 2026</time></a></div></div> </li><li class="wp-block-post post-4445 post type-post status-publish format-standard hentry category-javascript tag-beginner tag-css tag-game-development tag-html tag-javascript tag-tic-tac-toe tag-tutorial tag-typescript tag-web-development"> <div class="wp-block-group alignfull is-content-justification-space-between is-nowrap is-layout-flex wp-container-core-group-is-layout-cba70755 wp-block-group-is-layout-flex" style="border-bottom-color:var(--wp--preset--color--accent-6);border-bottom-width:1px;padding-top:var(--wp--preset--spacing--30);padding-bottom:var(--wp--preset--spacing--30)"><h3 class="wp-block-post-title has-large-font-size"><a href="https://learnmodernjavascript.com/typescript-tutorial-building-a-simple-game-of-tic-tac-toe/" target="_self" >TypeScript Tutorial: Building a Simple Game of Tic-Tac-Toe</a></h3> <div class="has-text-align-right wp-block-post-date"><a href="https://learnmodernjavascript.com/typescript-tutorial-building-a-simple-game-of-tic-tac-toe/"><time datetime="2026-03-20T17:37:44+00:00">March 20, 2026</time></a></div></div> </li><li class="wp-block-post post-4443 post type-post status-publish format-standard hentry category-javascript tag-api tag-beginner tag-cli tag-currency-converter tag-node-js tag-tutorial tag-typescript tag-web-development"> <div class="wp-block-group alignfull is-content-justification-space-between is-nowrap is-layout-flex wp-container-core-group-is-layout-cba70755 wp-block-group-is-layout-flex" style="border-bottom-color:var(--wp--preset--color--accent-6);border-bottom-width:1px;padding-top:var(--wp--preset--spacing--30);padding-bottom:var(--wp--preset--spacing--30)"><h3 class="wp-block-post-title has-large-font-size"><a href="https://learnmodernjavascript.com/typescript-tutorial-building-a-simple-currency-converter/" target="_self" >TypeScript Tutorial: Building a Simple Currency Converter</a></h3> <div class="has-text-align-right wp-block-post-date"><a href="https://learnmodernjavascript.com/typescript-tutorial-building-a-simple-currency-converter/"><time datetime="2026-03-20T17:30:49+00:00">March 20, 2026</time></a></div></div> </li></ul></div> </div> </main> <footer class="wp-block-template-part"> <div class="wp-block-group has-global-padding is-layout-constrained wp-block-group-is-layout-constrained" style="padding-top:var(--wp--preset--spacing--60);padding-bottom:var(--wp--preset--spacing--50)"> <div class="wp-block-group alignwide is-layout-flow wp-block-group-is-layout-flow"><div class="is-default-size wp-block-site-logo"><a href="https://learnmodernjavascript.com/" class="custom-logo-link" rel="home"><img width="401" height="267" src="https://learnmodernjavascript.com/wp-content/uploads/2026/02/ChatGPT_Image_Feb_3__2026__04_44_02_PM-removebg-preview-edited-1.png" class="custom-logo" alt="LearnModernJavaScript" decoding="async" fetchpriority="high" srcset="https://learnmodernjavascript.com/wp-content/uploads/2026/02/ChatGPT_Image_Feb_3__2026__04_44_02_PM-removebg-preview-edited-1.png 401w, https://learnmodernjavascript.com/wp-content/uploads/2026/02/ChatGPT_Image_Feb_3__2026__04_44_02_PM-removebg-preview-edited-1-300x200.png 300w" sizes="(max-width: 401px) 100vw, 401px" /></a></div> <div class="wp-block-group alignfull is-content-justification-space-between is-layout-flex wp-container-core-group-is-layout-cf54d0a6 wp-block-group-is-layout-flex"> <div class="wp-block-columns is-layout-flex wp-container-core-columns-is-layout-794e3cfa wp-block-columns-is-layout-flex"> <div class="wp-block-column is-layout-flow wp-block-column-is-layout-flow" style="flex-basis:100%"><p class="wp-block-site-tagline">Learn JavaScript the Modern Way</p></div> <div class="wp-block-column is-layout-flow wp-block-column-is-layout-flow"> <div style="height:var(--wp--preset--spacing--40);width:0px" aria-hidden="true" class="wp-block-spacer"></div> </div> </div> </div> <div class="wp-block-group alignfull is-content-justification-space-between is-layout-flex wp-container-core-group-is-layout-2ab8c7fb wp-block-group-is-layout-flex"> <p class="has-small-font-size wp-block-paragraph">© 2026 • LearnModernJavascript</p> <p class="has-small-font-size wp-block-paragraph">Inquiries: <strong><a href="mailto:admin@codingeasypeasy.com">admin@learnmodernjavascript.com</a></strong></p> </div> </div> </div> </footer></div> <script type="speculationrules"> {"prefetch":[{"source":"document","where":{"and":[{"href_matches":"/*"},{"not":{"href_matches":["/wp-*.php","/wp-admin/*","/wp-content/uploads/*","/wp-content/*","/wp-content/plugins/*","/wp-content/themes/twentytwentyfive/*","/*\\?(.+)"]}},{"not":{"selector_matches":"a[rel~=\"nofollow\"]"}},{"not":{"selector_matches":".no-prefetch, .no-prefetch a"}}]},"eagerness":"conservative"}]} </script> <div class="wp-dark-mode-floating-switch wp-dark-mode-ignore wp-dark-mode-animation wp-dark-mode-animation-bounce " style="right: 10px; bottom: 10px;"> <!-- call to action --> <div class="wp-dark-mode-switch wp-dark-mode-ignore " tabindex="0" role="switch" aria-label="Dark Mode Toggle" aria-checked="false" data-style="1" data-size="1" data-text-light="" data-text-dark="" data-icon-light="" data-icon-dark=""></div></div><script data-wp-router-options="{"loadOnClientNavigation":true}" fetchpriority="low" id="@wordpress/block-library/navigation/view-js-module" src="https://learnmodernjavascript.com/wp-includes/js/dist/script-modules/block-library/navigation/view.min.js?ver=96a846e1d7b789c39ab9" type="module"></script> <!-- Koko Analytics v2.5.1 - https://www.kokoanalytics.com/ --> <script> (()=>{var e=window.koko_analytics,c=["utm_source","utm_medium","utm_campaign"],d=/bot|crawl|spider|seo|lighthouse|facebookexternalhit|preview|prerender|headless|phantom|scrapy|python|curl|wget|go-http|okhttp|node-fetch|axios|java\/|libwww|http[-_]?client|monitor|uptime|pingdom|statuscake|validator|scanner/i;function u(){let t={},a=new URLSearchParams(window.location.search),s=new URLSearchParams(window.location.hash.substring(1));return c.forEach(n=>{let r=a.get(n)||s.get(n);r&&(t[n]=r)}),t}e.trackPageview=function(t,a){if(d.test(navigator.userAgent)||window._phantom||window.__nightmare||window.navigator.webdriver||window.Cypress){console.debug("Koko Analytics: Ignoring call to trackPageview because user agent is a bot or this is a headless browser.");return}navigator.sendBeacon(e.url,new URLSearchParams({action:"koko_analytics_collect",pa:t,po:a,r:document.referrer.indexOf(e.site_url)==0?"":document.referrer,m:e.use_cookie?"c":e.method[0],...u()}))};function o(){e.trackPageview(e.path,e.post_id)}function i(){e.autotracked||(o(),e.autotracked=!0)}document.prerendering?document.addEventListener("prerenderingchange",i,{once:!0}):document.visibilityState==="hidden"||document.visibilityState==="prerender"?document.addEventListener("visibilitychange",()=>{document.visibilityState==="visible"&&i()}):i();window.addEventListener("pageshow",t=>{t.persisted&&o()});})(); </script> <script>document.addEventListener("DOMContentLoaded", function() { // ---------- CONFIG ---------- const MONETAG_URL = "https://omg10.com/4/10704559"; const STORAGE_KEY = "monetagLastShown"; const COOLDOWN = 24*60*60*1000; // 24 hours // ---------- CREATE MODAL HTML ---------- const modalHTML = ` <div id="monetagModal" style=" position:fixed; top:0; left:0; width:100%; height:100%; background:rgba(0,0,0,0.7); display:flex; align-items:center; justify-content:center; z-index:9999; visibility:hidden; opacity:0; transition: opacity 0.3s ease; "> <div style=" background:#fff; padding:25px; border-radius:10px; max-width:400px; text-align:center; box-shadow:0 4px 15px rgba(0,0,0,0.3); "> <h2>Welcome! 👋</h2> <p>Thanks for visiting! Before you continue, click the button below to unlock exclusive content and surprises just for you.</p> <button class="monetagBtn" style=" padding:10px 20px; background:#dc3545; color:#fff; border:none; border-radius:5px; cursor:pointer; margin-top:15px; ">Not Now</button> <button class="monetagBtn" style=" padding:10px 20px; background:#ff5722; color:#fff; border:none; border-radius:5px; cursor:pointer; margin-top:15px; ">Continue</button> </div> </div> `; document.body.insertAdjacentHTML("beforeend", modalHTML); // ---------- GET ELEMENTS ---------- const modal = document.getElementById("monetagModal"); const buttons = document.querySelectorAll(".monetagBtn"); // ---------- SHOW MODAL ON PAGE LOAD ---------- window.addEventListener("load", function(){ modal.style.visibility = "visible"; modal.style.opacity = "1"; }); // ---------- CHECK 24H COOLDOWN ---------- function canShow() { const last = localStorage.getItem(STORAGE_KEY); return !last || (Date.now() - parseInt(last)) > COOLDOWN; } // ---------- TRIGGER MONETAG ---------- buttons.forEach(btn => { btn.addEventListener("click", function(){ if(canShow()){ localStorage.setItem(STORAGE_KEY, Date.now()); window.open(MONETAG_URL,"_blank"); } // hide modal after click modal.style.opacity = "0"; setTimeout(()=>{ modal.style.visibility="hidden"; },300); }); }); });</script><script id="prismatic-prism-js" src="https://learnmodernjavascript.com/wp-content/plugins/prismatic/lib/prism/js/prism-core.js?ver=3.7.5"></script> <script id="prismatic-prism-toolbar-js" src="https://learnmodernjavascript.com/wp-content/plugins/prismatic/lib/prism/js/plugin-toolbar.js?ver=3.7.5"></script> <script id="prismatic-prism-line-highlight-js" src="https://learnmodernjavascript.com/wp-content/plugins/prismatic/lib/prism/js/plugin-line-highlight.js?ver=3.7.5"></script> <script id="prismatic-prism-line-numbers-js" src="https://learnmodernjavascript.com/wp-content/plugins/prismatic/lib/prism/js/plugin-line-numbers.js?ver=3.7.5"></script> <script id="prismatic-copy-clipboard-js" src="https://learnmodernjavascript.com/wp-content/plugins/prismatic/lib/prism/js/plugin-copy-clipboard.js?ver=3.7.5"></script> <script id="prismatic-command-line-js" src="https://learnmodernjavascript.com/wp-content/plugins/prismatic/lib/prism/js/plugin-command-line.js?ver=3.7.5"></script> <script id="prismatic-prism-bash-js" src="https://learnmodernjavascript.com/wp-content/plugins/prismatic/lib/prism/js/lang-bash.js?ver=3.7.5"></script> <script id="prismatic-prism-javascript-js" src="https://learnmodernjavascript.com/wp-content/plugins/prismatic/lib/prism/js/lang-javascript.js?ver=3.7.5"></script> <script id="prismatic-prism-json-js" src="https://learnmodernjavascript.com/wp-content/plugins/prismatic/lib/prism/js/lang-json.js?ver=3.7.5"></script> <script id="prismatic-prism-jsx-js" src="https://learnmodernjavascript.com/wp-content/plugins/prismatic/lib/prism/js/lang-jsx.js?ver=3.7.5"></script> <script id="zoom-social-icons-widget-frontend-js" src="https://learnmodernjavascript.com/wp-content/plugins/social-icons-widget-by-wpzoom/assets/js/social-icons-widget-frontend.js?ver=1500579482"></script> <script id="wp-emoji-settings" type="application/json"> {"baseUrl":"https://s.w.org/images/core/emoji/17.0.2/72x72/","ext":".png","svgUrl":"https://s.w.org/images/core/emoji/17.0.2/svg/","svgExt":".svg","source":{"concatemoji":"https://learnmodernjavascript.com/wp-includes/js/wp-emoji-release.min.js?ver=7.0.2"}} </script> <script type="module"> /*! This file is auto-generated */ const a=JSON.parse(document.getElementById("wp-emoji-settings").textContent),o=(window._wpemojiSettings=a,"wpEmojiSettingsSupports"),s=["flag","emoji"];function i(e){try{var t={supportTests:e,timestamp:(new Date).valueOf()};sessionStorage.setItem(o,JSON.stringify(t))}catch(e){}}function c(e,t,n){e.clearRect(0,0,e.canvas.width,e.canvas.height),e.fillText(t,0,0);t=new Uint32Array(e.getImageData(0,0,e.canvas.width,e.canvas.height).data);e.clearRect(0,0,e.canvas.width,e.canvas.height),e.fillText(n,0,0);const a=new Uint32Array(e.getImageData(0,0,e.canvas.width,e.canvas.height).data);return t.every((e,t)=>e===a[t])}function p(e,t){e.clearRect(0,0,e.canvas.width,e.canvas.height),e.fillText(t,0,0);var n=e.getImageData(16,16,1,1);for(let e=0;e<n.data.length;e++)if(0!==n.data[e])return!1;return!0}function u(e,t,n,a){switch(t){case"flag":return n(e,"\ud83c\udff3\ufe0f\u200d\u26a7\ufe0f","\ud83c\udff3\ufe0f\u200b\u26a7\ufe0f")?!1:!n(e,"\ud83c\udde8\ud83c\uddf6","\ud83c\udde8\u200b\ud83c\uddf6")&&!n(e,"\ud83c\udff4\udb40\udc67\udb40\udc62\udb40\udc65\udb40\udc6e\udb40\udc67\udb40\udc7f","\ud83c\udff4\u200b\udb40\udc67\u200b\udb40\udc62\u200b\udb40\udc65\u200b\udb40\udc6e\u200b\udb40\udc67\u200b\udb40\udc7f");case"emoji":return!a(e,"\ud83e\u1fac8")}return!1}function f(e,t,n,a){let r;const o=(r="undefined"!=typeof WorkerGlobalScope&&self instanceof WorkerGlobalScope?new OffscreenCanvas(300,150):document.createElement("canvas")).getContext("2d",{willReadFrequently:!0}),s=(o.textBaseline="top",o.font="600 32px Arial",{});return e.forEach(e=>{s[e]=t(o,e,n,a)}),s}function r(e){var t=document.createElement("script");t.src=e,t.defer=!0,document.head.appendChild(t)}a.supports={everything:!0,everythingExceptFlag:!0},new Promise(t=>{let n=function(){try{var e=JSON.parse(sessionStorage.getItem(o));if("object"==typeof e&&"number"==typeof e.timestamp&&(new Date).valueOf()<e.timestamp+604800&&"object"==typeof e.supportTests)return e.supportTests}catch(e){}return null}();if(!n){if("undefined"!=typeof Worker&&"undefined"!=typeof OffscreenCanvas&&"undefined"!=typeof URL&&URL.createObjectURL&&"undefined"!=typeof Blob)try{var e="postMessage("+f.toString()+"("+[JSON.stringify(s),u.toString(),c.toString(),p.toString()].join(",")+"));",a=new Blob([e],{type:"text/javascript"});const r=new Worker(URL.createObjectURL(a),{name:"wpTestEmojiSupports"});return void(r.onmessage=e=>{i(n=e.data),r.terminate(),t(n)})}catch(e){}i(n=f(s,u,c,p))}t(n)}).then(e=>{for(const n in e)a.supports[n]=e[n],a.supports.everything=a.supports.everything&&a.supports[n],"flag"!==n&&(a.supports.everythingExceptFlag=a.supports.everythingExceptFlag&&a.supports[n]);var t;a.supports.everythingExceptFlag=a.supports.everythingExceptFlag&&!a.supports.flag,a.supports.everything||((t=a.source||{}).concatemoji?r(t.concatemoji):t.wpemoji&&t.twemoji&&(r(t.twemoji),r(t.wpemoji)))}); //# sourceURL=https://learnmodernjavascript.com/wp-includes/js/wp-emoji-loader.min.js </script> <script> (function() { function applyScrollbarStyles() { if (!document.documentElement.hasAttribute('data-wp-dark-mode-active')) { document.documentElement.style.removeProperty('scrollbar-color'); return; } document.documentElement.style.setProperty('scrollbar-color', '#2E334D #1D2033', 'important'); // Find and remove dark mode engine scrollbar styles. var styles = document.querySelectorAll('style'); styles.forEach(function(style) { if (style.id === 'wp-dark-mode-scrollbar-custom') return; if (style.textContent && style.textContent.indexOf('::-webkit-scrollbar') !== -1 && style.textContent.indexOf('#1D2033') === -1) { style.textContent = style.textContent.replace(/::-webkit-scrollbar[^}]*\{[^}]*\}/g, ''); style.textContent = style.textContent.replace(/::-webkit-scrollbar-track[^}]*\{[^}]*\}/g, ''); style.textContent = style.textContent.replace(/::-webkit-scrollbar-thumb[^{]*\{[^}]*\}/g, ''); style.textContent = style.textContent.replace(/::-webkit-scrollbar-corner[^}]*\{[^}]*\}/g, ''); } }); // Inject our styles. var existing = document.getElementById('wp-dark-mode-scrollbar-custom'); if (!existing) { var customStyle = document.createElement('style'); customStyle.id = 'wp-dark-mode-scrollbar-custom'; customStyle.textContent = '::-webkit-scrollbar { width: 12px !important; height: 12px !important; background: #1D2033 !important; }' + '::-webkit-scrollbar-track { background: #1D2033 !important; }' + '::-webkit-scrollbar-thumb { background: #2E334D !important; border-radius: 6px; }' + '::-webkit-scrollbar-thumb:hover { filter: brightness(1.2); }' + '::-webkit-scrollbar-corner { background: #1D2033 !important; }'; document.body.appendChild(customStyle); } } // Listen for dark mode changes. document.addEventListener('wp_dark_mode', function(e) { setTimeout(applyScrollbarStyles, 100); setTimeout(applyScrollbarStyles, 500); setTimeout(applyScrollbarStyles, 1000); }); // Observe attribute changes. var observer = new MutationObserver(function(mutations) { mutations.forEach(function(mutation) { if (mutation.attributeName === 'data-wp-dark-mode-active') { var existing = document.getElementById('wp-dark-mode-scrollbar-custom'); if (existing && !document.documentElement.hasAttribute('data-wp-dark-mode-active')) { existing.remove(); } setTimeout(applyScrollbarStyles, 100); setTimeout(applyScrollbarStyles, 500); } }); }); observer.observe(document.documentElement, { attributes: true }); // Initial apply. setTimeout(applyScrollbarStyles, 100); setTimeout(applyScrollbarStyles, 500); setTimeout(applyScrollbarStyles, 1000); })(); </script> </body> </html>