How ZDF created a video PWA with offline and dark mode

Learn how ZDF created a progressive web app (PWA) with modern features like offline support, installability, and dark mode.

Martin Schierle
Martin Schierle
Scott Friesen
Scott Friesen

When broadcaster ZDF was considering redesigning their frontend technology stack, they decided to take a closer look at Progressive Web Apps for their streaming site ZDFmediathek. Development agency Cellular took on the challenge to build a web-based experience that is on par with ZDF's platform-specific iOS and Android apps. The PWA offers installability, offline video playback, transition animations, and a dark mode.

Adding a service worker

A key feature of a PWA is offline support. For ZDF most of the heavy lifting is done by Workbox, a set of libraries and Node modules that make it easy to support different caching strategies. The ZDF PWA is built with TypeScript and React, so it uses the Workbox library already built into create-react-app to precache static assets. This lets the application focus on making the dynamic content available offline, in this case the videos and their metadata.

The basic idea is quite simple: fetch the video and store it as a blob in IndexedDB. Then during playback, listen for online/offline events, and switch to the downloaded version when the device goes offline.

Unfortunately things turned out to be a little more complex. One of the project requirements was to use the official ZDF web player which doesn't provide any offline support. The player takes a content ID as input, talks to the ZDF API, and plays back the associated video.

This is where one of the web's most powerful features comes to the rescue: service workers.

The service worker can intercept the various requests done by the player and respond with the data from IndexedDB. This transparently adds offline capabilities without having to change a single line of the player's code.

Since offline videos tend to be quite large, a big question is how many of them can actually be stored on a device. With the help of the StorageManager API the app can estimate the available space and inform the user when there is insufficient space before even starting the download. Unfortunately Safari isn't on the list of browsers implementing this API and at the time of writing there wasn't much up-to-date information about how other browsers applied quotas. Therefore, the team wrote a small utility to test the behavior on various devices. By now a comprehensive article exists that sums up all the details.

Adding a custom install prompt

The ZDF PWA offers a custom in-app installation flow and prompts users to install the app as soon as they want to download their first video. This is a good point in time to prompt for install because the user has expressed a clear intention to use the app offline.

Custom invitation to install. Custom install prompt being triggered when downloading a video for offline consumption.
Custom install prompt being triggered when downloading a video for offline consumption.

Building an offline page to access downloads

When the device is not connected to the internet and the user navigates to a page that is not available in offline mode, a special page is shown instead that lists all videos that have previously been downloaded or (in case no content has been downloaded yet) a short explanation of the offline feature.

Offline page showing all content available for watching offline. Offline page showing that no content is available for watching offline.
Offline page showing all content available for watching offline.

Using frame loading rate for adaptive features

To offer a rich user experience the ZDF PWA includes some subtle transitions that happen when the user scrolls or navigates. On low-end devices such animations usually have the opposite effect and make the app feel sluggish and less responsive if they don't run at 60 frames per second. To take this into account the app measures the actual frame rate via requestAnimationFrame() while the application loads and disables all animations when the value drops below a certain threshold.

const frameRate = new Promise(resolve => {
  let lastTick;
  const samples = [];

  function measure() {
    const tick =;
    if (lastTick) samples.push(tick - lastTick);
    lastTick = tick;
    if (samples.length < 20) requestAnimationFrame(measure);
    else {
      const avg = samples.reduce((a, b) => a + b) / samples.length;
      const fps = 1000 / avg;

Even if this measurement provides only a rough indication of the device's performance and varies on each load, it was still a good basis for decision-making. It's worth mentioning that depending on the use case there are other techniques for adaptive loading that developers can implement. One great advantage of this approach is that it is available on all platforms.

Dark mode

A popular feature for modern mobile experiences is dark mode. Especially when watching videos in low ambient light many people prefer a dimmed UI. The ZDF PWA not only provides a switch that allows users to toggle between a light and a dark theme, it also reacts to changes of the OS-wide color preferences. This way the app will automatically change its appearance on devices that have set up a schedule to change the theme base on the time of day.


The new progressive web app was silently launched as a public beta in March 2020 and has received a lot of positive feedback since then. While the beta phase continues, the PWA still runs under its own temporary domain. Even though the PWA wasn't publicly promoted there is a steadily growing number of users. Many of these are from the Microsoft Store which allows Windows 10 users to discover PWAs and install them like platform-specific apps.

What's next?

ZDF plans to continue adding features to their PWA, including login for personalization, cross-device and platform viewing, and push notifications.