Progressive web apps (PWAs) are applications that look and behave like mobile apps, but are built using web technologies. While they leverage native mobile features, you access them through the browser like regular web applications, meaning you don’t have to download them via mobile app marketplaces.
PWAs have gained popularity in recent years thanks to their ability to combine web and mobile features. This ability provides users with a seamless experience, even when the application is offline, and even enables users to install the web app to their device like a native mobile app.
Remix is a robust, modern web framework that makes it easy to build performant, scalable apps. We can use Remix PWA — a lightweight, standalone framework — to combine the benefits of Remix and PWAs.
In this article, we will discuss Remix PWA, why it’s necessary, and how to integrate it into our Remix app with a demo. You can check out the complete code for our demo app on GitLab.
The Replay is a weekly newsletter for dev and engineering leaders.
Delivered once a week, it's your curated guide to the most important conversations around frontend dev, emerging AI tools, and the state of modern software.
Remix is a modern, full-stack web framework that extends the capabilities of the React library. It’s designed to help developers build highly responsive, blazing-fast web applications on the server. Some of its key features include, among many others:
As an SSR framework, Remix handles everything on the server before sending the information back to the user. This results in a faster and more interactive user experience. It’s worth noting that navigation and certain other interactions still occur on the client side, contributing to a smooth UX.
Remix PWA is a lightweight PWA framework that integrates the features of a PWA into a Remix application. These features include caching, offline support, push notifications, and so on.
Combining PWA principles with the Remix web framework enables us to create web apps that offer a more reliable, performant user experience, making Remix PWA an important tool in your developer toolbox. Here are some of the features that Remix PWA provides:
Now that we’ve seen the benefits of Remix and PWAs and the power of leveraging them together using the Remix PWA framework, let’s see how we can build our own Remix PWA.
Before we dive into the tutorial part of this article, ensure that you have the following installed:
You should also already have basic knowledge of Remix, as this article will focus mainly on building the PWA. If you don’t, you can check out this guide to the Remix framework to familiarize yourself first. Otherwise, let’s jump right in.
We will start by creating a new Remix project. Run the command below:
npx create-remix@latest
Follow the prompts to configure your project. You can also use the command below to spin up your Remix project:
npx create-remix@latest remix-pwa-tutorial
The command above creates a new Remix project with the name remix-pwa-tutorial and uses the basic template provided in the docs.
For Yarn users, note that when accepting the prompts, you should select No for the Install dependencies with npm? prompt. This ensures that you can install the dependencies and run the Remix project with Yarn instead of npm:

After completing the installation process, navigate into your project, run your yarn install command, and then you’re ready to go. Spin up the project with any of the commands below:
/* npm */ npm run dev /* yarn */ yarn run dev
I’m not using a template, so you can use this command to install your Remix app:
npx create-remix@latest
Then, replace the contents of your root.tsx file with the following:
// root.tsx
import type { LinksFunction } from "@remix-run/node";
import {
Form,
Link,
Links,
Meta,
Outlet,
Scripts,
useLoaderData,
ScrollRestoration,
} from "@remix-run/react";
import { json } from '@remix-run/node';
import appStylesHref from './app.css';
import { getContacts } from './data';
import { useSWEffect, LiveReload } from '@remix-pwa/sw';
export const links: LinksFunction = () => [
...([{ rel: "stylesheet", href: appStylesHref }]),
];
export const loader = async () => {
const contacts = await getContacts();
return json({ contacts });
};
export default function App() {
useSWEffect()
const { contacts } = useLoaderData<typeof loader>();
return (
<html lang="en">
<head>
<meta charSet="utf-8" />
<meta
name="viewport"
content="width=device-width, initial-scale=1"
/>
<Meta />
<link rel="manifest" href="/manifest[.]webmanifest" />
<Links />
</head>
<body>
<div id="sidebar">
<h1>Remix Contacts</h1>
<div>
<Form
id="search-form"
role="search"
>
<input
id="q"
aria-label="Search contacts"
placeholder="Search"
type="search"
name="q"
/>
<div
id="search-spinner"
aria-hidden
hidden={true}
/>
</Form>
<Form method="post">
<button type="submit">New</button>
</Form>
</div>
<nav>
{contacts.length ? (
<ul>
{contacts.map((contact) => (
<li key={contact.id}>
<Link to={`contacts/${contact.id}`}>
{contact.first || contact.last ? (
<>
{contact.first} {contact.last}
</>
) : (
<i>No Name</i>
)}
{contact.favorite ? <span>★</span> : null}
</Link>
</li>
))}
</ul>
) : (
<p>
<i>No contacts</i>
</p>
)}
</nav>
</div>
<div id="detail">
<Outlet />
</div>
<ScrollRestoration />
<Scripts />
<LiveReload />
</body>
</html>
);
}
The code above sets up a simple application for managing contact information.
Next, in your app directory, create a routes directory, then create a contacts.$contactId.tsx file inside the new directory. Copy the code below into this file:
// routes/contacts.$contactId.tsx
import type { LoaderFunctionArgs } from '@remix-run/node';
import { Form, useLoaderData } from '@remix-run/react';
import type { FunctionComponent } from 'react';
import type { ContactRecord } from '../data';
import { json } from '@remix-run/node';
import invariant from 'tiny-invariant';
import { getContact } from '../data';
export const loader = async ({ params }: LoaderFunctionArgs) => {
invariant(params.contactId, 'Missing contactId param');
const contact = await getContact(params.contactId);
if (!contact) {
throw new Response('Not Found', { status: 404 });
}
return json({ contact });
};
export default function Contact() {
const { contact } = useLoaderData<typeof loader>();
return (
<div id="contact">
<div>
<img
alt={`${contact.first} ${contact.last} avatar`}
key={contact.avatar}
src={contact.avatar}
/>
</div>
<div>
<h1>
{contact.first || contact.last ? (
<>
{contact.first} {contact.last}
</>
) : (
<i>No Name</i>
)}{' '}
<Favorite contact={contact} />
</h1>
{contact.twitter ? (
<p>
<a href={`https://twitter.com/${contact.twitter}`}>
{contact.twitter}
</a>
</p>
) : null}
{contact.notes ? <p>{contact.notes}</p> : null}
<div>
<Form action="edit">
<button type="submit">Edit</button>
</Form>
<Form
action="destroy"
method="post"
onSubmit={(event) => {
const response = confirm(
'Please confirm you want to delete this record.'
);
if (!response) {
event.preventDefault();
}
}}
>
<button type="submit">Delete</button>
</Form>
</div>
</div>
</div>
);
}
const Favorite: FunctionComponent<{
contact: Pick<ContactRecord, 'favorite'>;
}> = ({ contact }) => {
const favorite = contact.favorite;
return (
<Form method="post">
<button
aria-label={favorite ? 'Remove from favorites' : 'Add to favorites'}
name="favorite"
value={favorite ? 'false' : 'true'}
>
{favorite ? '★' : '☆'}
</button>
</Form>
);
};
The code above sets up a contact details page. Spin up your app, and you should have something like this:

As you can see, we’ve set up a simple contacts management app using Remix. Selecting a contact displays details about that contact and allows you to edit the information or delete the contact. You can also use the search bar to filter your list of contacts.
Now that we have our Remix app set up, let’s see how to add PWA features.
We’ll use the remix-pwa package to integrate PWA into our Remix application. As we discussed before, this lightweight package allows us to transform our web app into a Remix PWA with very little effort and few configurations.
The remix-pwa package provides a scaffolding template that adds the needed PWA files to our application, including a service worker and a manifest.json file. Let’s explore these in a little more detail.
A service worker is a script that runs in the background and allows our application to work offline. Simply put, it allows offline access to your application. Here’s a snippet of what a service worker looks like:
// src/index.tsx
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/sw.js')
.then((registration) => {
console.log('Service Worker registered:', registration.scope);
})
.catch((error) => {
console.error('Service Worker registration failed:', error);
});
}
Meanwhile, a web app manifest is a JSON file that defines your PWA’s metadata, including its name, icons, and other properties. After installing remix-pwa, you can find this file in your routes directory.
Remix PWA integrates the manifest.json file into the service worker setup automatically during the installation process. Here’s a snippet of what a manifest.json file looks like:
{
"name": "My Remix PWA",
"short_name": "Remix PWA",
"description": "A Remix Progressive Web App",
"start_url": "/",
"display": "standalone",
"background_color": "#ffffff",
"theme_color": "#000000",
"icons": [
{
"src": "/icons/icon-192x192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "/icons/icon-512x512.png",
"sizes": "512x512",
"type": "image/png"
}
]
}
We’ve explored the PWA features we want to add to our Remix app. Now, let’s go through the specific steps to install and configure these features using the remix-pwa package.
remix-pwaTo install the remix-pwa package, run the command below:
npx remix-pwa@latest init
This command will install the Remix CLI, which we will use to scaffold a new Remix PWA application. Follow the prompts and select the PWA features or options you want to integrate into your application, like so:

As you can see above, these options include the language, workbox integration, precaching, and so on.
After installation, we need to configure or update our Remix configuration to use our service worker. In your remix.config.ts file, add the following to the top of your file:
/** @type {import('@remix-pwa/dev').WorkerConfig} */
This configuration allows Remix PWA to work with your Remix application.
Next, head over to your entry.client.tsx file and add the code below:
// entry.client.tsx
import { loadServiceWorker } from "@remix-pwa/sw";
loadServiceWorker();
This registers your service worker so that your application can locate your service worker file, which is the entry.worker.tsx file in the routes directory.
Next, in your root.tsx file, import the useSWEffect Hook. This Hook shares some similarities with the useEffect Hook, but is used specifically to listen and inform your service worker of any changes or events:
// root.tsx
import { useSWEffect } from "@remix-pwa/sw";
export default function App() {
useSWEffect();
return (
<html lang="en">
....
</html>
);
}
Note that, as is the rule with any other Hook, you have to call the Hook from inside your function component.
Continuing to work inside your root.ts file, replace the existing LiveReload component with a new LiveReload component imported from "@remix-pwa/sw":
// root.tsx
import { useSWEffect, LiveReload } from "@remix-pwa/sw";
....
This LiveReload component performs the same function as the Remix LiveReload component, but provides support for Remix PWA. It basically listens to file changes and reloads the page. In our Remix PWA’s case, it also updates the service worker.
Lastly, add your manifest path before the Links component within the head tag:
<link rel="manifest" href="/location of your manifest" />
Your completed root file should somewhat look like this:
// root.tsx
import { cssBundleHref } from "@remix-run/css-bundle";
import { useSWEffect, LiveReload } from "@remix-pwa/sw";
import type { LinksFunction } from "@remix-run/node";
import {
Links,
Meta,
Outlet,
Scripts,
ScrollRestoration,
} from "@remix-run/react";
// ...
export default function App() {
useSWEffect();
return (
<html lang="en">
<head>
<meta charSet="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<Meta />
<link rel="manifest" href="/manifest.webmanifest" />
<Links />
</head>
<body>
// ....
</body>
</html>
);
}
If your manifest is saying that the route is not matching, I recommend that you move your manifest.webmanifest.ts file to the root of your file. Then, make sure your link path is accurately specified as well.
Another common issue is the inaccurate import of buffer in node_modules. If you experience this, head over to the node_modules/@remix-pwa/cache/dist/src/cache.js file and replace your buffer import with the following:
// node_modules/@remix-pwa/cache/dist/src/cache.js import * as B from 'buffer/index.js';
Finally, build and serve your application by running these commands:
// build your application npm run build // serve your application npm run start
That concludes it. We now have a working Remix PWA. You can inspect the Application tab in your web browser to see your app manifest and service workers up and running. Here’s what you should see in the Service workers sub-tab:

Meanwhile, here’s what you should see in your App Manifest sub-tab:

In the Lighthouse tab, you can also run the PWA analysis to be sure your application is compatible with PWA requirements:

Earlier, we discussed how one of the benefits of PWAs is that they leverage native mobile features, meaning they can look and behave like mobile apps even though they’re web apps. Right now, our contacts management app still looks like a web app, but it’s easy to create a mobile-like version. Let’s see how.
After building your project, checking that the manifest and service workers are up and running, and running your analysis in the Lighthouse tab, you can install your PWA by clicking on the Install button on the right side of your search input bar. This will create a mobile-like app for your website.
The result should look something like the below, which feels similar to a native app:

That’s it! You can also check out the full code for this project on GitLab.
Remix has gained traction and popularity for its developer-friendly and performance approach. However, it’s also good to note some of the limitations associated with building a PWA with Remix.
Having built PWAs with React, I can say it edges over Remix in terms of complexity and developer experience. While the Remix PWA framework makes it easier to build a PWA with Remix, building a PWA with React can be more straightforward and easier to implement in comparison.
First of all, the learning curve of Remix PWA isn’t as easy as React’s. Since Remix is relatively new, you might run into some issues, as highlighted in this article.
Also, while the Remix community is growing, it’s still smaller than frameworks like React or Next.js. This means that the community support for building PWAs with Remix will be small too. If you have questions while working on your project, it may not be easy to find answers or solutions.
Since Remix has a smaller community, the availability of other articles and documentation is also limited. There are very few tutorials on how to build a PWA with Remix, so that might be challenging for developers. React, in comparison, has several articles and tutorials along with comprehensive docs.
Meanwhile, when you run into bugs and issues while working on a React PWA, there is a huge, thriving developer ecosystem available to help out.
Finally, Remix has a great DX, as it handles code-splitting, SSR, and more automatically, making it easier to develop highly performant apps. On the other hand, building a PWA with React doesn’t require several configurations as we’ve seen in this tutorial, which can provide a smoother DX.
As usual, when it comes to choosing the right option for your project, it’s important to assess your needs and go with the option that suits them best. You can use this table as a quick reference regarding the differences and similarities between building a PWA with React vs. using Remix PWA:
| Remix | React | |
|---|---|---|
| Learning curve | Easier with handling complex features | More straightforward and easier to implement |
| Community | Growing, but smaller than React’s | Large and thriving community |
| Documentation/tutorials | Limited | Extensive |
| DX | Great — automatically handles many features, including route-based data fetching and error handling | Great — doesn’t require as many configurations as Remix PWA |
| Code splitting support | Yes, handled automatically | Supports code splitting but requires additional setup |
| SSR | Yes, handled automatically | Requires additional config |
| Performance | Well-optimized by default | Not optimized by default |
| Routing mechanism | SSR | CSR; also supports SSR |
| Data handling | Automatically handles simple data | Flexible data handling. Also supports plugins or third party intergrations like Redux to enhance data handling capabilities |
| Best for… | Projects that needs automatic data handling | Any complex or big projects. BMW, Twitter, Starbucks, and other well-known companies have all used React PWA |
In this article, we looked at what a progressive web app is, why it’s important to know how to build a Remix PWA, and how to do so using the remix-pwa package.
Building a PWA can give your users a better and more engaging web experience. Combining Remix’s capabilities with PWA features is a great way to support this purpose.
However, depending on your needs, you may find React to be the better choice for your PWA project. It’s important to look at the complexity, DX, and flexibility of each tool to determine which one will best support you in developing fast, performant PWAs with all the features you need.
I hope you enjoyed this article. If you have any questions, feel free to comment them below. All the best!
Install LogRocket via npm or script tag. LogRocket.init() must be called client-side, not
server-side
$ npm i --save logrocket
// Code:
import LogRocket from 'logrocket';
LogRocket.init('app/id');
// Add to your HTML:
<script src="https://cdn.lr-ingest.com/LogRocket.min.js"></script>
<script>window.LogRocket && window.LogRocket.init('app/id');</script>

:has(), with examplesThe CSS :has() pseudo-class is a powerful new feature that lets you style parents, siblings, and more – writing cleaner, more dynamic CSS with less JavaScript.

Kombai AI converts Figma designs into clean, responsive frontend code. It helps developers build production-ready UIs faster while keeping design accuracy and code quality intact.

Discover what’s new in The Replay, LogRocket’s newsletter for dev and engineering leaders, in the October 22nd issue.

John Reilly discusses how software development has been changed by the innovations of AI: both the positives and the negatives.
Hey there, want to help make our blog better?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up now