A PWA is Still a Simple Website Under the Hood

Yurii Tokarskyi

Aug 10, 2021 • 11 min read
Old cell phone, smartphone and modern smartphone in the hand

Progressive web applications are still treated as a new thing. Many people talk about new browser APIs in the context of PWAs. But when we run for the newest things we forget about the basics.

Look at the Fugu API tracker — it's a roadmap of new features in Chromium. The ambitions are impressive. Because of such things many people think PWAs are a different category of web product. But:

A PWA is still a simple website first.

It’s a web application second.

And only in the third place it’s progressive.

What basics?

People can use web apps in different environments: browsers, web-views, or iframes. Browsers are modern and old with different API support. Web-views or iframes have limitations too. Also, don't forget about different viewports: from the smallest watches and mobile devices to desktop browsers with 8K screens.

Only web developers create things for such diverse environments. The current state of web technologies allows us to make both mobile and desktop users happy. But:

With great power
comes great responsibility.

Uncle Ben

Let’s remember two concepts — graceful degradation and progressive enhancement:

Graceful degradation — a design philosophy that centers around trying to build a modern web site/application that will work in the newest browsers, but falls back to an experience that while not as good still delivers essential content and functionality in older browsers.

MDN

Progressive enhancement — a design philosophy that provides a baseline of essential content and functionality to as many users as possible, while delivering the best possible experience only to users of the most modern browsers that can run all the required code.

MDN

These two are different concepts. But their meaning is the same. We need to make web applications accessible on older or unusual devices. On modern devices, we can make them full-powered.

Website comparison on iPhone XR and new Nokia 3310. Manuel Matuzović

How to make sure a web application is usable on various devices and environments? A frontend developer should ask theirself 3 what-ifs:

  1. What if JavaScript isn’t supported?
  2. What if specific features or APIs aren't supported?
  3. What if specific properties aren't supported?

What if JavaScript isn’t supported?

It’s not only the user’s fault when JavaScript isn’t supported. The script may fail to download. A browser extension may break it. Or internal firewall mistakes can block external scripts.

It’s not a matter of if your users don’t have JavaScript — it’s a matter of when and how often.

Adam Silver

A common mistake is omitting server rendering, forms processing and navigation. Can I see a simple webpage if I don’t have JavaScript? Can I navigate the application? Can I send the form without JavaScript?

We need to stop breaking native behavior. Native behaviour is the default behavior of the browser for specific HTML tags. For example:

  • The a tag updates history and allows one to use CMD/CTRL to open links in a new tab;
  • The form tag has native support for sending data to a server using POST/GET requests;
  • The button tag has default centreing of internal content.

Using a div tag as a button is a mistake. Using input without parent form too. These mistakes break native behavior — we need to make sure such things work.

After that we can make our application sexier with JavaScript. Use client-side navigation. Send a form with AJAX. Or update content in real-time.


/* ☝️ Simple form in React without and with native behavior support */

/* ❌ — client-side only form, default behavior of form is broken */

const Form: FC = ({ onSend }) => {

const [value, setValue] = useState("");

return (

<div> {/* 👎 */}

<input value={value} onChange={e => setValue(e.target.value)} />

<button>Save</button> {/* 👎 */}

</div>

);

}

/* ✅ — server-side processing supported, enhanced on client side */

const Form: FC = ({ onSend }) => {

const [value, setValue] = useState("");

return (

<form {/* 👉 */} action="/form-processing" method="POST">

<input value={value} onChange={e => setValue(e.target.value)} />

<button {/* 👉 */} type="submit">Save</button>

</form>

);

}

What if a specific feature or API isn’t supported?

Let’s take a look at Vibration API support: you can use it on Android, but on iPhone, you can not. Even worse — calling window.navigator.vibrate() in Safari will cause

TypeError: navigator.vibrate is not a function. That error may break the application.

We need to ask ourselves: what if it’s not supported?

In the case of Vibration API, we can omit that. I don’t think the user will close an application because of vibration missing.

/* ☝️ Using Vibration API safely */


/* ❌ — causes TypeError: navigator.vibrate is not a function in Safari */

window.navigator.vibrate(100);


/* ✅ — simply omits in browser withput Vibration API support */

if ("vibrate" in navigator) {

window.navigator.vibrate(100);

}

But if we have no access to the Geolocation API — what will be the state of the application? We need to inform the user if it's not supported. And we need to make sure the user can continue the scenario without that.

What to do in case of a more crucial feature, like Indexed DB?

We need to add fallback support, even if it’s less performant or optimized. Use it as caching storage only — fetch data from the server and cache it to omit future network requests. When no support of Indexed DB is available, the application continues to fetch data from the server.

We can also use other feature detection solutions, like Modernizr. But I want to pay attention to the mindset, not the tools — don’t be optimistic and think about how to handle an error first.

What if a specific property isn’t supported?

Specific property support is almost the same as feature/API support. The only difference is that it's more painful. If the full feature is not supported we can check it in a second. In case when a property is not supported or missing we spend more time finding the mismatch.

Let’s take a look at the Notifications API. It’s supported in all modern browsers — only Safari requires a custom configuration. We can live with it. Check how to get notification permissions. All browsers return a Promise: only Safari requires a callback as a parameter.

/* ☝️ Safely request permission for notifications */

function handlePermission(permission) {

/* ... */

}

if ("Notification" in window) {

Notification

.requestPermission(handlePermission) /* for Safari */

.then(handlePermission) /* other browsers */

} else {

/* handle case when notification aren't supported */

}

Most cases of unsupported properties I know apply to CSS. Each CSS property has browser-specific support. What is nice about CSS is that we can provide many values for the same property. The browser omits unsupported values and uses the previous one.

For example, height. First, we provide a percentage value. It's supported everywhere. On the next line, we add a new value: vh (viewport height). If 100vh isn’t supported — the browser uses 100%.

/* ☝️ Using 100vh with fallback to 100% in CSS */
/* ❌ — not all browsers support 100vh */

.root {

height: 100vh;

}

/* ✅ — every browser supports 100%, modern browser uses 100vh */

.root {

height: 100%;

height: 100vh;

}

From enhancement to inclusion

Accessibility comes from supporting unexpected use cases. It’s especially applicable to the web. People can use our application on various devices and browsers. They can even see it embedded in other applications. As much as we care about the environments we support, following the principles I described above prepares us for inclusive design.

Screen readers work well with native browser behavior. We need to extend our products with a few things for great accessibility. The SEO team will be happy if our application works without JavaScript. People with broken JavaScript support will be happy. As well as people with older systems, browsers or devices. Just ask yourself 3 questions during development:

  1. What if JavaScript isn’t supported?
  2. What if specific features or APIs aren't supported?
  3. What if specific properties aren't supported?

I’ll agree if you say: "We have a budget, no time to play these games". I don't want to say we need to achieve everything above. We need to ask the right questions and check the facts. When we don't do something like server rendering, we need to be aware of it.

Progressively enhanced (or gracefully degraded) systems are better prepared to welcome all the users — let’s care about them.

More posts by this author

Yurii Tokarskyi

codestories