CINXE.COM
How ZDF created a video PWA with offline and dark mode | web.dev
<!doctype html> <html lang="en" dir="ltr"> <head> <meta name="google-signin-client-id" content="157101835696-ooapojlodmuabs2do2vuhhnf90bccmoi.apps.googleusercontent.com"> <meta name="google-signin-scope" content="profile email https://www.googleapis.com/auth/developerprofiles https://www.googleapis.com/auth/developerprofiles.award"> <meta property="og:site_name" content="web.dev"> <meta property="og:type" content="website"><meta name="theme-color" content="#3740ff"><meta charset="utf-8"> <meta content="IE=Edge" http-equiv="X-UA-Compatible"> <meta name="viewport" content="width=device-width, initial-scale=1"> <link rel="manifest" href="/_pwa/web/manifest.json" crossorigin="use-credentials"> <link rel="preconnect" href="//www.gstatic.com" crossorigin> <link rel="preconnect" href="//fonts.gstatic.com" crossorigin> <link rel="preconnect" href="//fonts.googleapis.com" crossorigin> <link rel="preconnect" href="//apis.google.com" crossorigin> <link rel="preconnect" href="//www.google-analytics.com" crossorigin><link rel="stylesheet" href="//fonts.googleapis.com/css?family=Google+Sans:400,500|Roboto:400,400italic,500,500italic,700,700italic|Roboto+Mono:400,500,700&display=swap"> <link rel="stylesheet" href="//fonts.googleapis.com/css2?family=Material+Icons&family=Material+Symbols+Outlined&display=block"><link rel="stylesheet" href="https://www.gstatic.com/devrel-devsite/prod/v870e399c64f7c43c99a3043db4b3a74327bb93d0914e84a0c3dba90bbfd67625/web/css/app.css"> <link rel="stylesheet" href="https://www.gstatic.com/devrel-devsite/prod/v870e399c64f7c43c99a3043db4b3a74327bb93d0914e84a0c3dba90bbfd67625/web/css/dark-theme.css" disabled> <link rel="shortcut icon" href="https://www.gstatic.com/devrel-devsite/prod/v870e399c64f7c43c99a3043db4b3a74327bb93d0914e84a0c3dba90bbfd67625/web/images/favicon.png"> <link rel="apple-touch-icon" href="https://www.gstatic.com/devrel-devsite/prod/v870e399c64f7c43c99a3043db4b3a74327bb93d0914e84a0c3dba90bbfd67625/web/images/touchicon-180.png"><link rel="canonical" href="https://web.dev/case-studies/zdf"><link rel="search" type="application/opensearchdescription+xml" title="web.dev" href="https://web.dev/s/opensearch.xml"> <link rel="alternate" hreflang="en" href="https://web.dev/case-studies/zdf" /><link rel="alternate" hreflang="x-default" href="https://web.dev/case-studies/zdf" /><link rel="alternate" hreflang="ar" href="https://web.dev/case-studies/zdf?hl=ar" /><link rel="alternate" hreflang="bn" href="https://web.dev/case-studies/zdf?hl=bn" /><link rel="alternate" hreflang="zh-Hans" href="https://web.dev/case-studies/zdf?hl=zh-cn" /><link rel="alternate" hreflang="zh-Hant" href="https://web.dev/case-studies/zdf?hl=zh-tw" /><link rel="alternate" hreflang="fa" href="https://web.dev/case-studies/zdf?hl=fa" /><link rel="alternate" hreflang="fr" href="https://web.dev/case-studies/zdf?hl=fr" /><link rel="alternate" hreflang="de" href="https://web.dev/case-studies/zdf?hl=de" /><link rel="alternate" hreflang="he" href="https://web.dev/case-studies/zdf?hl=he" /><link rel="alternate" hreflang="hi" href="https://web.dev/case-studies/zdf?hl=hi" /><link rel="alternate" hreflang="id" href="https://web.dev/case-studies/zdf?hl=id" /><link rel="alternate" hreflang="it" href="https://web.dev/case-studies/zdf?hl=it" /><link rel="alternate" hreflang="ja" href="https://web.dev/case-studies/zdf?hl=ja" /><link rel="alternate" hreflang="ko" href="https://web.dev/case-studies/zdf?hl=ko" /><link rel="alternate" hreflang="pl" href="https://web.dev/case-studies/zdf?hl=pl" /><link rel="alternate" hreflang="pt-BR" href="https://web.dev/case-studies/zdf?hl=pt-br" /><link rel="alternate" hreflang="ru" href="https://web.dev/case-studies/zdf?hl=ru" /><link rel="alternate" hreflang="es-419" href="https://web.dev/case-studies/zdf?hl=es-419" /><link rel="alternate" hreflang="th" href="https://web.dev/case-studies/zdf?hl=th" /><link rel="alternate" hreflang="tr" href="https://web.dev/case-studies/zdf?hl=tr" /><link rel="alternate" hreflang="vi" href="https://web.dev/case-studies/zdf?hl=vi" /><link rel="alternate" hreflang="en-cn" href="https://web.developers.google.cn/case-studies/zdf" /><link rel="alternate" hreflang="x-default" href="https://web.developers.google.cn/case-studies/zdf" /><link rel="alternate" hreflang="ar-cn" href="https://web.developers.google.cn/case-studies/zdf?hl=ar" /><link rel="alternate" hreflang="bn-cn" href="https://web.developers.google.cn/case-studies/zdf?hl=bn" /><link rel="alternate" hreflang="zh-Hans-cn" href="https://web.developers.google.cn/case-studies/zdf?hl=zh-cn" /><link rel="alternate" hreflang="zh-Hant-cn" href="https://web.developers.google.cn/case-studies/zdf?hl=zh-tw" /><link rel="alternate" hreflang="fa-cn" href="https://web.developers.google.cn/case-studies/zdf?hl=fa" /><link rel="alternate" hreflang="fr-cn" href="https://web.developers.google.cn/case-studies/zdf?hl=fr" /><link rel="alternate" hreflang="de-cn" href="https://web.developers.google.cn/case-studies/zdf?hl=de" /><link rel="alternate" hreflang="he-cn" href="https://web.developers.google.cn/case-studies/zdf?hl=he" /><link rel="alternate" hreflang="hi-cn" href="https://web.developers.google.cn/case-studies/zdf?hl=hi" /><link rel="alternate" hreflang="id-cn" href="https://web.developers.google.cn/case-studies/zdf?hl=id" /><link rel="alternate" hreflang="it-cn" href="https://web.developers.google.cn/case-studies/zdf?hl=it" /><link rel="alternate" hreflang="ja-cn" href="https://web.developers.google.cn/case-studies/zdf?hl=ja" /><link rel="alternate" hreflang="ko-cn" href="https://web.developers.google.cn/case-studies/zdf?hl=ko" /><link rel="alternate" hreflang="pl-cn" href="https://web.developers.google.cn/case-studies/zdf?hl=pl" /><link rel="alternate" hreflang="pt-BR-cn" href="https://web.developers.google.cn/case-studies/zdf?hl=pt-br" /><link rel="alternate" hreflang="ru-cn" href="https://web.developers.google.cn/case-studies/zdf?hl=ru" /><link rel="alternate" hreflang="es-419-cn" href="https://web.developers.google.cn/case-studies/zdf?hl=es-419" /><link rel="alternate" hreflang="th-cn" href="https://web.developers.google.cn/case-studies/zdf?hl=th" /><link rel="alternate" hreflang="tr-cn" href="https://web.developers.google.cn/case-studies/zdf?hl=tr" /><link rel="alternate" hreflang="vi-cn" href="https://web.developers.google.cn/case-studies/zdf?hl=vi" /><title>How ZDF created a video PWA with offline and dark mode | web.dev</title> <meta property="og:title" content="How ZDF created a video PWA with offline and dark mode | web.dev"><meta name="description" content="Learn how ZDF created a progressive web app (PWA) with modern features like offline support, installability, and dark mode."> <meta property="og:description" content="Learn how ZDF created a progressive web app (PWA) with modern features like offline support, installability, and dark mode."><meta property="og:url" content="https://web.dev/case-studies/zdf"><meta property="og:image" content="https://web.dev/static/images/social-wide.jpg"> <meta property="og:image:width" content="1200"> <meta property="og:image:height" content="675"><meta property="og:locale" content="en"><meta name="twitter:card" content="summary"><meta name="twitter:image" content="https://web.dev/static/images/social-wide.jpg"><meta name="twitter:site" content="@ChromiumDev"> <link rel="stylesheet" href="/extras.css"></head> <body class="" template="page" theme="web-theme" type="case-study" appearance layout="docs" display-toc pending> <devsite-progress type="indeterminate" id="app-progress"></devsite-progress> <section class="devsite-wrapper"> <devsite-cookie-notification-bar></devsite-cookie-notification-bar><devsite-header role="banner"> <div class="devsite-header--inner nocontent"> <div class="devsite-top-logo-row-wrapper-wrapper"> <div class="devsite-top-logo-row-wrapper"> <div class="devsite-top-logo-row"> <button type="button" id="devsite-hamburger-menu" class="devsite-header-icon-button button-flat material-icons gc-analytics-event" data-category="Site-Wide Custom Events" data-label="Navigation menu button" visually-hidden aria-label="Open menu"> </button> <div class="devsite-product-name-wrapper"> <a href="/" class="devsite-site-logo-link gc-analytics-event" data-category="Site-Wide Custom Events" data-label="Site logo" track-type="globalNav" track-name="webDev" track-metadata-position="nav" track-metadata-eventDetail="nav"> <picture> <source srcset="https://www.gstatic.com/devrel-devsite/prod/v870e399c64f7c43c99a3043db4b3a74327bb93d0914e84a0c3dba90bbfd67625/web/images/lockup-dark-theme.svg" media="(prefers-color-scheme: dark)" class="devsite-dark-theme" alt="web.dev"> <img src="https://www.gstatic.com/devrel-devsite/prod/v870e399c64f7c43c99a3043db4b3a74327bb93d0914e84a0c3dba90bbfd67625/web/images/lockup.svg" class="devsite-site-logo" alt="web.dev"> </picture> </a> <span class="devsite-product-name"> <ul class="devsite-breadcrumb-list" > <li class="devsite-breadcrumb-item "> </li> </ul> </span> </div> <div class="devsite-top-logo-row-middle"> <div class="devsite-header-upper-tabs"> <devsite-tabs class="upper-tabs"> <nav class="devsite-tabs-wrapper" aria-label="Upper tabs"> <tab > <a href="https://web.dev/about" track-metadata-eventdetail="https://web.dev/about" class="devsite-tabs-content gc-analytics-event " track-type="nav" track-metadata-position="nav - about" track-metadata-module="primary nav" data-category="Site-Wide Custom Events" data-label="Tab: About" track-name="about" > About </a> </tab> <tab > <a href="https://web.dev/html" track-metadata-eventdetail="https://web.dev/html" class="devsite-tabs-content gc-analytics-event " track-type="nav" track-metadata-position="nav - html" track-metadata-module="primary nav" data-category="Site-Wide Custom Events" data-label="Tab: HTML" track-name="html" > HTML </a> </tab> <tab > <a href="https://web.dev/css" track-metadata-eventdetail="https://web.dev/css" class="devsite-tabs-content gc-analytics-event " track-type="nav" track-metadata-position="nav - css" track-metadata-module="primary nav" data-category="Site-Wide Custom Events" data-label="Tab: CSS" track-name="css" > CSS </a> </tab> <tab > <a href="https://web.dev/javascript" track-metadata-eventdetail="https://web.dev/javascript" class="devsite-tabs-content gc-analytics-event " track-type="nav" track-metadata-position="nav - javascript" track-metadata-module="primary nav" data-category="Site-Wide Custom Events" data-label="Tab: JavaScript" track-name="javascript" > JavaScript </a> </tab> <tab > <a href="https://web.dev/blog" track-metadata-eventdetail="https://web.dev/blog" class="devsite-tabs-content gc-analytics-event " track-type="nav" track-metadata-position="nav - blog" track-metadata-module="primary nav" data-category="Site-Wide Custom Events" data-label="Tab: Blog" track-name="blog" > Blog </a> </tab> <tab > <a href="https://web.dev/learn" track-metadata-eventdetail="https://web.dev/learn" class="devsite-tabs-content gc-analytics-event " track-type="nav" track-metadata-position="nav - learn" track-metadata-module="primary nav" data-category="Site-Wide Custom Events" data-label="Tab: Learn" track-name="learn" > Learn </a> </tab> <tab class="devsite-active"> <a href="https://web.dev/explore" track-metadata-eventdetail="https://web.dev/explore" class="devsite-tabs-content gc-analytics-event " track-type="nav" track-metadata-position="nav - explore" track-metadata-module="primary nav" aria-label="Explore, selected" data-category="Site-Wide Custom Events" data-label="Tab: Explore" track-name="explore" > Explore </a> </tab> <tab > <a href="https://web.dev/patterns" track-metadata-eventdetail="https://web.dev/patterns" class="devsite-tabs-content gc-analytics-event " track-type="nav" track-metadata-position="nav - patterns" track-metadata-module="primary nav" data-category="Site-Wide Custom Events" data-label="Tab: Patterns" track-name="patterns" > Patterns </a> </tab> <tab > <a href="https://web.dev/case-studies" track-metadata-eventdetail="https://web.dev/case-studies" class="devsite-tabs-content gc-analytics-event " track-type="nav" track-metadata-position="nav - case studies" track-metadata-module="primary nav" data-category="Site-Wide Custom Events" data-label="Tab: Case studies" track-name="case studies" > Case studies </a> </tab> </nav> </devsite-tabs> </div> <devsite-search enable-signin enable-search enable-suggestions enable-query-completion project-name="web.dev" tenant-name="web.dev" > <form class="devsite-search-form" action="https://web.dev/s/results" method="GET"> <div class="devsite-search-container"> <button type="button" search-open class="devsite-search-button devsite-header-icon-button button-flat material-icons" aria-label="Open search"></button> <div class="devsite-searchbox"> <input aria-activedescendant="" aria-autocomplete="list" aria-label="Search" aria-expanded="false" aria-haspopup="listbox" autocomplete="off" class="devsite-search-field devsite-search-query" name="q" placeholder="Search" role="combobox" type="text" value="" > <div class="devsite-search-image material-icons" aria-hidden="true"> </div> <div class="devsite-search-shortcut-icon-container" aria-hidden="true"> <kbd class="devsite-search-shortcut-icon">/</kbd> </div> </div> </div> </form> <button type="button" search-close class="devsite-search-button devsite-header-icon-button button-flat material-icons" aria-label="Close search"></button> </devsite-search> </div> <devsite-appearance-selector></devsite-appearance-selector> <devsite-language-selector> <ul role="presentation"> <li role="presentation"> <a role="menuitem" lang="en" >English</a> </li> <li role="presentation"> <a role="menuitem" lang="de" >Deutsch</a> </li> <li role="presentation"> <a role="menuitem" lang="es_419" >Español – América Latina</a> </li> <li role="presentation"> <a role="menuitem" lang="fr" >Français</a> </li> <li role="presentation"> <a role="menuitem" lang="id" >Indonesia</a> </li> <li role="presentation"> <a role="menuitem" lang="it" >Italiano</a> </li> <li role="presentation"> <a role="menuitem" lang="pl" >Polski</a> </li> <li role="presentation"> <a role="menuitem" lang="pt_br" >Português – Brasil</a> </li> <li role="presentation"> <a role="menuitem" lang="vi" >Tiếng Việt</a> </li> <li role="presentation"> <a role="menuitem" lang="tr" >Türkçe</a> </li> <li role="presentation"> <a role="menuitem" lang="ru" >Русский</a> </li> <li role="presentation"> <a role="menuitem" lang="he" >עברית</a> </li> <li role="presentation"> <a role="menuitem" lang="ar" >العربيّة</a> </li> <li role="presentation"> <a role="menuitem" lang="fa" >فارسی</a> </li> <li role="presentation"> <a role="menuitem" lang="hi" >हिंदी</a> </li> <li role="presentation"> <a role="menuitem" lang="bn" >বাংলা</a> </li> <li role="presentation"> <a role="menuitem" lang="th" >ภาษาไทย</a> </li> <li role="presentation"> <a role="menuitem" lang="zh_cn" >中文 – 简体</a> </li> <li role="presentation"> <a role="menuitem" lang="zh_tw" >中文 – 繁體</a> </li> <li role="presentation"> <a role="menuitem" lang="ja" >日本語</a> </li> <li role="presentation"> <a role="menuitem" lang="ko" >한국어</a> </li> </ul> </devsite-language-selector> <devsite-user enable-profiles id="devsite-user"> <span class="button devsite-top-button" aria-hidden="true" visually-hidden>Sign in</span> </devsite-user> </div> </div> </div> <div class="devsite-collapsible-section "> <div class="devsite-header-background"> <div class="devsite-product-id-row" > <div class="devsite-product-description-row"> <ul class="devsite-breadcrumb-list" > <li class="devsite-breadcrumb-item "> <a href="https://web.dev/explore" class="devsite-breadcrumb-link gc-analytics-event" data-category="Site-Wide Custom Events" data-label="Lower Header" data-value="1" track-type="globalNav" track-name="breadcrumb" track-metadata-position="1" track-metadata-eventdetail="" > Collections </a> </li> </ul> </div> </div> <div class="devsite-doc-set-nav-row"> <devsite-tabs class="lower-tabs"> <nav class="devsite-tabs-wrapper" aria-label="Lower tabs"> <tab > <a href="https://web.dev/explore/learn-core-web-vitals" track-metadata-eventdetail="https://web.dev/explore/learn-core-web-vitals" class="devsite-tabs-content gc-analytics-event " track-type="nav" track-metadata-position="nav - core web vitals" track-metadata-module="primary nav" data-category="Site-Wide Custom Events" data-label="Tab: Core Web Vitals" track-name="core web vitals" > Core Web Vitals </a> </tab> <tab > <a href="https://web.dev/explore/metrics" track-metadata-eventdetail="https://web.dev/explore/metrics" class="devsite-tabs-content gc-analytics-event " track-type="nav" track-metadata-position="nav - metrics" track-metadata-module="primary nav" data-category="Site-Wide Custom Events" data-label="Tab: Metrics" track-name="metrics" > Metrics </a> </tab> <tab > <a href="https://web.dev/explore/fast" track-metadata-eventdetail="https://web.dev/explore/fast" class="devsite-tabs-content gc-analytics-event " track-type="nav" track-metadata-position="nav - fast load times" track-metadata-module="primary nav" data-category="Site-Wide Custom Events" data-label="Tab: Fast load times" track-name="fast load times" > Fast load times </a> </tab> <tab > <a href="https://web.dev/explore/ai" track-metadata-eventdetail="https://web.dev/explore/ai" class="devsite-tabs-content gc-analytics-event " track-type="nav" track-metadata-position="nav - ai" track-metadata-module="primary nav" data-category="Site-Wide Custom Events" data-label="Tab: AI" track-name="ai" > AI </a> </tab> <tab > <a href="https://web.dev/explore/how-to-optimize-inp" track-metadata-eventdetail="https://web.dev/explore/how-to-optimize-inp" class="devsite-tabs-content gc-analytics-event " track-type="nav" track-metadata-position="nav - optimize interaction to next paint (inp)" track-metadata-module="primary nav" data-category="Site-Wide Custom Events" data-label="Tab: Optimize Interaction to Next Paint (INP)" track-name="optimize interaction to next paint (inp)" > Optimize Interaction to Next Paint (INP) </a> </tab> <tab class="devsite-active"> <a href="https://web.dev/explore/progressive-web-apps" track-metadata-eventdetail="https://web.dev/explore/progressive-web-apps" class="devsite-tabs-content gc-analytics-event " track-type="nav" track-metadata-position="nav - progressive web apps" track-metadata-module="primary nav" aria-label="Progressive Web Apps, selected" data-category="Site-Wide Custom Events" data-label="Tab: Progressive Web Apps" track-name="progressive web apps" > Progressive Web Apps </a> </tab> <tab > <a href="https://web.dev/accessibility" track-metadata-eventdetail="https://web.dev/accessibility" class="devsite-tabs-content gc-analytics-event " track-type="nav" track-metadata-position="nav - accessible to all" track-metadata-module="primary nav" data-category="Site-Wide Custom Events" data-label="Tab: Accessible to all" track-name="accessible to all" > Accessible to all </a> </tab> <tab > <a href="https://web.dev/explore/reliable" track-metadata-eventdetail="https://web.dev/explore/reliable" class="devsite-tabs-content gc-analytics-event " track-type="nav" track-metadata-position="nav - network reliability" track-metadata-module="primary nav" data-category="Site-Wide Custom Events" data-label="Tab: Network reliability" track-name="network reliability" > Network reliability </a> </tab> <tab > <a href="https://web.dev/explore/secure" track-metadata-eventdetail="https://web.dev/explore/secure" class="devsite-tabs-content gc-analytics-event " track-type="nav" track-metadata-position="nav - safe and secure" track-metadata-module="primary nav" data-category="Site-Wide Custom Events" data-label="Tab: Safe and secure" track-name="safe and secure" > Safe and secure </a> </tab> <tab > <a href="https://web.dev/explore/discoverable" track-metadata-eventdetail="https://web.dev/explore/discoverable" class="devsite-tabs-content gc-analytics-event " track-type="nav" track-metadata-position="nav - easily discoverable" track-metadata-module="primary nav" data-category="Site-Wide Custom Events" data-label="Tab: Easily discoverable" track-name="easily discoverable" > Easily discoverable </a> </tab> <tab > <a href="https://web.dev/explore/payments" track-metadata-eventdetail="https://web.dev/explore/payments" class="devsite-tabs-content gc-analytics-event " track-type="nav" track-metadata-position="nav - web payments" track-metadata-module="primary nav" data-category="Site-Wide Custom Events" data-label="Tab: Web Payments" track-name="web payments" > Web Payments </a> </tab> <tab > <a href="https://web.dev/explore/media" track-metadata-eventdetail="https://web.dev/explore/media" class="devsite-tabs-content gc-analytics-event " track-type="nav" track-metadata-position="nav - media" track-metadata-module="primary nav" data-category="Site-Wide Custom Events" data-label="Tab: Media" track-name="media" > Media </a> </tab> <tab > <a href="https://web.dev/explore/devices" track-metadata-eventdetail="https://web.dev/explore/devices" class="devsite-tabs-content gc-analytics-event " track-type="nav" track-metadata-position="nav - devices" track-metadata-module="primary nav" data-category="Site-Wide Custom Events" data-label="Tab: Devices" track-name="devices" > Devices </a> </tab> <tab > <a href="https://web.dev/explore/animations" track-metadata-eventdetail="https://web.dev/explore/animations" class="devsite-tabs-content gc-analytics-event " track-type="nav" track-metadata-position="nav - animations" track-metadata-module="primary nav" data-category="Site-Wide Custom Events" data-label="Tab: Animations" track-name="animations" > Animations </a> </tab> <tab > <a href="https://web.dev/explore/identity" track-metadata-eventdetail="https://web.dev/explore/identity" class="devsite-tabs-content gc-analytics-event " track-type="nav" track-metadata-position="nav - identity" track-metadata-module="primary nav" data-category="Site-Wide Custom Events" data-label="Tab: Identity" track-name="identity" > Identity </a> </tab> <tab > <a href="https://web.dev/explore/webassembly" track-metadata-eventdetail="https://web.dev/explore/webassembly" class="devsite-tabs-content gc-analytics-event " track-type="nav" track-metadata-position="nav - webassembly" track-metadata-module="primary nav" data-category="Site-Wide Custom Events" data-label="Tab: WebAssembly" track-name="webassembly" > WebAssembly </a> </tab> <tab > <a href="https://web.dev/explore/test-automation" track-metadata-eventdetail="https://web.dev/explore/test-automation" class="devsite-tabs-content gc-analytics-event " track-type="nav" track-metadata-position="nav - test automation" track-metadata-module="primary nav" data-category="Site-Wide Custom Events" data-label="Tab: Test automation" track-name="test automation" > Test automation </a> </tab> <tab > <a href="https://web.dev/explore/react" track-metadata-eventdetail="https://web.dev/explore/react" class="devsite-tabs-content gc-analytics-event " track-type="nav" track-metadata-position="nav - react" track-metadata-module="primary nav" data-category="Site-Wide Custom Events" data-label="Tab: React" track-name="react" > React </a> </tab> <tab > <a href="https://web.dev/explore/angular" track-metadata-eventdetail="https://web.dev/explore/angular" class="devsite-tabs-content gc-analytics-event " track-type="nav" track-metadata-position="nav - angular" track-metadata-module="primary nav" data-category="Site-Wide Custom Events" data-label="Tab: Angular" track-name="angular" > Angular </a> </tab> <tab > <a href="https://web.dev/explore/mini-apps" track-metadata-eventdetail="https://web.dev/explore/mini-apps" class="devsite-tabs-content gc-analytics-event " track-type="nav" track-metadata-position="nav - mini apps" track-metadata-module="primary nav" data-category="Site-Wide Custom Events" data-label="Tab: Mini apps" track-name="mini apps" > Mini apps </a> </tab> </nav> </devsite-tabs> </div> </div> </div> </div> </devsite-header> <devsite-book-nav scrollbars > <div class="devsite-book-nav-filter" > <span class="filter-list-icon material-icons" aria-hidden="true"></span> <input type="text" placeholder="Filter" aria-label="Type to filter" role="searchbox"> <span class="filter-clear-button hidden" data-title="Clear filter" aria-label="Clear filter" role="button" tabindex="0"></span> </div> <nav class="devsite-book-nav devsite-nav nocontent" aria-label="Side menu"> <div class="devsite-mobile-header"> <button type="button" id="devsite-close-nav" class="devsite-header-icon-button button-flat material-icons gc-analytics-event" data-category="Site-Wide Custom Events" data-label="Close navigation" aria-label="Close navigation"> </button> <div class="devsite-product-name-wrapper"> <a href="/" class="devsite-site-logo-link gc-analytics-event" data-category="Site-Wide Custom Events" data-label="Site logo" track-type="globalNav" track-name="webDev" track-metadata-position="nav" track-metadata-eventDetail="nav"> <picture> <source srcset="https://www.gstatic.com/devrel-devsite/prod/v870e399c64f7c43c99a3043db4b3a74327bb93d0914e84a0c3dba90bbfd67625/web/images/lockup-dark-theme.svg" media="(prefers-color-scheme: dark)" class="devsite-dark-theme" alt="web.dev"> <img src="https://www.gstatic.com/devrel-devsite/prod/v870e399c64f7c43c99a3043db4b3a74327bb93d0914e84a0c3dba90bbfd67625/web/images/lockup.svg" class="devsite-site-logo" alt="web.dev"> </picture> </a> <span class="devsite-product-name"> <ul class="devsite-breadcrumb-list" > <li class="devsite-breadcrumb-item "> </li> </ul> </span> </div> </div> <div class="devsite-book-nav-wrapper"> <div class="devsite-mobile-nav-top"> <ul class="devsite-nav-list"> <li class="devsite-nav-item"> <a href="/about" class="devsite-nav-title gc-analytics-event " data-category="Site-Wide Custom Events" data-label="Tab: About" track-name="about" data-category="Site-Wide Custom Events" data-label="Responsive Tab: About" track-type="globalNav" track-metadata-eventDetail="globalMenu" track-metadata-position="nav"> <span class="devsite-nav-text" tooltip > About </span> </a> </li> <li class="devsite-nav-item"> <a href="/html" class="devsite-nav-title gc-analytics-event " data-category="Site-Wide Custom Events" data-label="Tab: HTML" track-name="html" data-category="Site-Wide Custom Events" data-label="Responsive Tab: HTML" track-type="globalNav" track-metadata-eventDetail="globalMenu" track-metadata-position="nav"> <span class="devsite-nav-text" tooltip > HTML </span> </a> </li> <li class="devsite-nav-item"> <a href="/css" class="devsite-nav-title gc-analytics-event " data-category="Site-Wide Custom Events" data-label="Tab: CSS" track-name="css" data-category="Site-Wide Custom Events" data-label="Responsive Tab: CSS" track-type="globalNav" track-metadata-eventDetail="globalMenu" track-metadata-position="nav"> <span class="devsite-nav-text" tooltip > CSS </span> </a> </li> <li class="devsite-nav-item"> <a href="/javascript" class="devsite-nav-title gc-analytics-event " data-category="Site-Wide Custom Events" data-label="Tab: JavaScript" track-name="javascript" data-category="Site-Wide Custom Events" data-label="Responsive Tab: JavaScript" track-type="globalNav" track-metadata-eventDetail="globalMenu" track-metadata-position="nav"> <span class="devsite-nav-text" tooltip > JavaScript </span> </a> </li> <li class="devsite-nav-item"> <a href="/blog" class="devsite-nav-title gc-analytics-event " data-category="Site-Wide Custom Events" data-label="Tab: Blog" track-name="blog" data-category="Site-Wide Custom Events" data-label="Responsive Tab: Blog" track-type="globalNav" track-metadata-eventDetail="globalMenu" track-metadata-position="nav"> <span class="devsite-nav-text" tooltip > Blog </span> </a> </li> <li class="devsite-nav-item"> <a href="/learn" class="devsite-nav-title gc-analytics-event " data-category="Site-Wide Custom Events" data-label="Tab: Learn" track-name="learn" data-category="Site-Wide Custom Events" data-label="Responsive Tab: Learn" track-type="globalNav" track-metadata-eventDetail="globalMenu" track-metadata-position="nav"> <span class="devsite-nav-text" tooltip > Learn </span> </a> </li> <li class="devsite-nav-item"> <a href="/explore" class="devsite-nav-title gc-analytics-event devsite-nav-active" data-category="Site-Wide Custom Events" data-label="Tab: Explore" track-name="explore" data-category="Site-Wide Custom Events" data-label="Responsive Tab: Explore" track-type="globalNav" track-metadata-eventDetail="globalMenu" track-metadata-position="nav"> <span class="devsite-nav-text" tooltip > Explore </span> </a> <ul class="devsite-nav-responsive-tabs"> <li class="devsite-nav-item"> <a href="/explore/learn-core-web-vitals" class="devsite-nav-title gc-analytics-event devsite-nav-has-children " data-category="Site-Wide Custom Events" data-label="Tab: Core Web Vitals" track-name="core web vitals" data-category="Site-Wide Custom Events" data-label="Responsive Tab: Core Web Vitals" track-type="globalNav" track-metadata-eventDetail="globalMenu" track-metadata-position="nav"> <span class="devsite-nav-text" tooltip > Core Web Vitals </span> <span class="devsite-nav-icon material-icons" data-icon="forward" > </span> </a> </li> <li class="devsite-nav-item"> <a href="/explore/metrics" class="devsite-nav-title gc-analytics-event devsite-nav-has-children " data-category="Site-Wide Custom Events" data-label="Tab: Metrics" track-name="metrics" data-category="Site-Wide Custom Events" data-label="Responsive Tab: Metrics" track-type="globalNav" track-metadata-eventDetail="globalMenu" track-metadata-position="nav"> <span class="devsite-nav-text" tooltip > Metrics </span> <span class="devsite-nav-icon material-icons" data-icon="forward" > </span> </a> </li> <li class="devsite-nav-item"> <a href="/explore/fast" class="devsite-nav-title gc-analytics-event devsite-nav-has-children " data-category="Site-Wide Custom Events" data-label="Tab: Fast load times" track-name="fast load times" data-category="Site-Wide Custom Events" data-label="Responsive Tab: Fast load times" track-type="globalNav" track-metadata-eventDetail="globalMenu" track-metadata-position="nav"> <span class="devsite-nav-text" tooltip > Fast load times </span> <span class="devsite-nav-icon material-icons" data-icon="forward" > </span> </a> </li> <li class="devsite-nav-item"> <a href="/explore/ai" class="devsite-nav-title gc-analytics-event devsite-nav-has-children " data-category="Site-Wide Custom Events" data-label="Tab: AI" track-name="ai" data-category="Site-Wide Custom Events" data-label="Responsive Tab: AI" track-type="globalNav" track-metadata-eventDetail="globalMenu" track-metadata-position="nav"> <span class="devsite-nav-text" tooltip > AI </span> <span class="devsite-nav-icon material-icons" data-icon="forward" > </span> </a> </li> <li class="devsite-nav-item"> <a href="/explore/how-to-optimize-inp" class="devsite-nav-title gc-analytics-event devsite-nav-has-children " data-category="Site-Wide Custom Events" data-label="Tab: Optimize Interaction to Next Paint (INP)" track-name="optimize interaction to next paint (inp)" data-category="Site-Wide Custom Events" data-label="Responsive Tab: Optimize Interaction to Next Paint (INP)" track-type="globalNav" track-metadata-eventDetail="globalMenu" track-metadata-position="nav"> <span class="devsite-nav-text" tooltip > Optimize Interaction to Next Paint (INP) </span> <span class="devsite-nav-icon material-icons" data-icon="forward" > </span> </a> </li> <li class="devsite-nav-item"> <a href="/explore/progressive-web-apps" class="devsite-nav-title gc-analytics-event devsite-nav-has-children devsite-nav-active" data-category="Site-Wide Custom Events" data-label="Tab: Progressive Web Apps" track-name="progressive web apps" data-category="Site-Wide Custom Events" data-label="Responsive Tab: Progressive Web Apps" track-type="globalNav" track-metadata-eventDetail="globalMenu" track-metadata-position="nav"> <span class="devsite-nav-text" tooltip menu="_book"> Progressive Web Apps </span> <span class="devsite-nav-icon material-icons" data-icon="forward" menu="_book"> </span> </a> </li> <li class="devsite-nav-item"> <a href="/accessibility" class="devsite-nav-title gc-analytics-event devsite-nav-has-children " data-category="Site-Wide Custom Events" data-label="Tab: Accessible to all" track-name="accessible to all" data-category="Site-Wide Custom Events" data-label="Responsive Tab: Accessible to all" track-type="globalNav" track-metadata-eventDetail="globalMenu" track-metadata-position="nav"> <span class="devsite-nav-text" tooltip > Accessible to all </span> <span class="devsite-nav-icon material-icons" data-icon="forward" > </span> </a> </li> <li class="devsite-nav-item"> <a href="/explore/reliable" class="devsite-nav-title gc-analytics-event devsite-nav-has-children " data-category="Site-Wide Custom Events" data-label="Tab: Network reliability" track-name="network reliability" data-category="Site-Wide Custom Events" data-label="Responsive Tab: Network reliability" track-type="globalNav" track-metadata-eventDetail="globalMenu" track-metadata-position="nav"> <span class="devsite-nav-text" tooltip > Network reliability </span> <span class="devsite-nav-icon material-icons" data-icon="forward" > </span> </a> </li> <li class="devsite-nav-item"> <a href="/explore/secure" class="devsite-nav-title gc-analytics-event devsite-nav-has-children " data-category="Site-Wide Custom Events" data-label="Tab: Safe and secure" track-name="safe and secure" data-category="Site-Wide Custom Events" data-label="Responsive Tab: Safe and secure" track-type="globalNav" track-metadata-eventDetail="globalMenu" track-metadata-position="nav"> <span class="devsite-nav-text" tooltip > Safe and secure </span> <span class="devsite-nav-icon material-icons" data-icon="forward" > </span> </a> </li> <li class="devsite-nav-item"> <a href="/explore/discoverable" class="devsite-nav-title gc-analytics-event devsite-nav-has-children " data-category="Site-Wide Custom Events" data-label="Tab: Easily discoverable" track-name="easily discoverable" data-category="Site-Wide Custom Events" data-label="Responsive Tab: Easily discoverable" track-type="globalNav" track-metadata-eventDetail="globalMenu" track-metadata-position="nav"> <span class="devsite-nav-text" tooltip > Easily discoverable </span> <span class="devsite-nav-icon material-icons" data-icon="forward" > </span> </a> </li> <li class="devsite-nav-item"> <a href="/explore/payments" class="devsite-nav-title gc-analytics-event devsite-nav-has-children " data-category="Site-Wide Custom Events" data-label="Tab: Web Payments" track-name="web payments" data-category="Site-Wide Custom Events" data-label="Responsive Tab: Web Payments" track-type="globalNav" track-metadata-eventDetail="globalMenu" track-metadata-position="nav"> <span class="devsite-nav-text" tooltip > Web Payments </span> <span class="devsite-nav-icon material-icons" data-icon="forward" > </span> </a> </li> <li class="devsite-nav-item"> <a href="/explore/media" class="devsite-nav-title gc-analytics-event devsite-nav-has-children " data-category="Site-Wide Custom Events" data-label="Tab: Media" track-name="media" data-category="Site-Wide Custom Events" data-label="Responsive Tab: Media" track-type="globalNav" track-metadata-eventDetail="globalMenu" track-metadata-position="nav"> <span class="devsite-nav-text" tooltip > Media </span> <span class="devsite-nav-icon material-icons" data-icon="forward" > </span> </a> </li> <li class="devsite-nav-item"> <a href="/explore/devices" class="devsite-nav-title gc-analytics-event devsite-nav-has-children " data-category="Site-Wide Custom Events" data-label="Tab: Devices" track-name="devices" data-category="Site-Wide Custom Events" data-label="Responsive Tab: Devices" track-type="globalNav" track-metadata-eventDetail="globalMenu" track-metadata-position="nav"> <span class="devsite-nav-text" tooltip > Devices </span> <span class="devsite-nav-icon material-icons" data-icon="forward" > </span> </a> </li> <li class="devsite-nav-item"> <a href="/explore/animations" class="devsite-nav-title gc-analytics-event devsite-nav-has-children " data-category="Site-Wide Custom Events" data-label="Tab: Animations" track-name="animations" data-category="Site-Wide Custom Events" data-label="Responsive Tab: Animations" track-type="globalNav" track-metadata-eventDetail="globalMenu" track-metadata-position="nav"> <span class="devsite-nav-text" tooltip > Animations </span> <span class="devsite-nav-icon material-icons" data-icon="forward" > </span> </a> </li> <li class="devsite-nav-item"> <a href="/explore/identity" class="devsite-nav-title gc-analytics-event devsite-nav-has-children " data-category="Site-Wide Custom Events" data-label="Tab: Identity" track-name="identity" data-category="Site-Wide Custom Events" data-label="Responsive Tab: Identity" track-type="globalNav" track-metadata-eventDetail="globalMenu" track-metadata-position="nav"> <span class="devsite-nav-text" tooltip > Identity </span> <span class="devsite-nav-icon material-icons" data-icon="forward" > </span> </a> </li> <li class="devsite-nav-item"> <a href="/explore/webassembly" class="devsite-nav-title gc-analytics-event devsite-nav-has-children " data-category="Site-Wide Custom Events" data-label="Tab: WebAssembly" track-name="webassembly" data-category="Site-Wide Custom Events" data-label="Responsive Tab: WebAssembly" track-type="globalNav" track-metadata-eventDetail="globalMenu" track-metadata-position="nav"> <span class="devsite-nav-text" tooltip > WebAssembly </span> <span class="devsite-nav-icon material-icons" data-icon="forward" > </span> </a> </li> <li class="devsite-nav-item"> <a href="/explore/test-automation" class="devsite-nav-title gc-analytics-event devsite-nav-has-children " data-category="Site-Wide Custom Events" data-label="Tab: Test automation" track-name="test automation" data-category="Site-Wide Custom Events" data-label="Responsive Tab: Test automation" track-type="globalNav" track-metadata-eventDetail="globalMenu" track-metadata-position="nav"> <span class="devsite-nav-text" tooltip > Test automation </span> <span class="devsite-nav-icon material-icons" data-icon="forward" > </span> </a> </li> <li class="devsite-nav-item"> <a href="/explore/react" class="devsite-nav-title gc-analytics-event devsite-nav-has-children " data-category="Site-Wide Custom Events" data-label="Tab: React" track-name="react" data-category="Site-Wide Custom Events" data-label="Responsive Tab: React" track-type="globalNav" track-metadata-eventDetail="globalMenu" track-metadata-position="nav"> <span class="devsite-nav-text" tooltip > React </span> <span class="devsite-nav-icon material-icons" data-icon="forward" > </span> </a> </li> <li class="devsite-nav-item"> <a href="/explore/angular" class="devsite-nav-title gc-analytics-event devsite-nav-has-children " data-category="Site-Wide Custom Events" data-label="Tab: Angular" track-name="angular" data-category="Site-Wide Custom Events" data-label="Responsive Tab: Angular" track-type="globalNav" track-metadata-eventDetail="globalMenu" track-metadata-position="nav"> <span class="devsite-nav-text" tooltip > Angular </span> <span class="devsite-nav-icon material-icons" data-icon="forward" > </span> </a> </li> <li class="devsite-nav-item"> <a href="/explore/mini-apps" class="devsite-nav-title gc-analytics-event devsite-nav-has-children " data-category="Site-Wide Custom Events" data-label="Tab: Mini apps" track-name="mini apps" data-category="Site-Wide Custom Events" data-label="Responsive Tab: Mini apps" track-type="globalNav" track-metadata-eventDetail="globalMenu" track-metadata-position="nav"> <span class="devsite-nav-text" tooltip > Mini apps </span> <span class="devsite-nav-icon material-icons" data-icon="forward" > </span> </a> </li> </ul> </li> <li class="devsite-nav-item"> <a href="/patterns" class="devsite-nav-title gc-analytics-event " data-category="Site-Wide Custom Events" data-label="Tab: Patterns" track-name="patterns" data-category="Site-Wide Custom Events" data-label="Responsive Tab: Patterns" track-type="globalNav" track-metadata-eventDetail="globalMenu" track-metadata-position="nav"> <span class="devsite-nav-text" tooltip > Patterns </span> </a> </li> <li class="devsite-nav-item"> <a href="/case-studies" class="devsite-nav-title gc-analytics-event " data-category="Site-Wide Custom Events" data-label="Tab: Case studies" track-name="case studies" data-category="Site-Wide Custom Events" data-label="Responsive Tab: Case studies" track-type="globalNav" track-metadata-eventDetail="globalMenu" track-metadata-position="nav"> <span class="devsite-nav-text" tooltip > Case studies </span> </a> </li> </ul> </div> <div class="devsite-mobile-nav-bottom"> <ul class="devsite-nav-list" menu="_book"> <li class="devsite-nav-item devsite-nav-heading"><div class="devsite-nav-title devsite-nav-title-no-path"> <span class="devsite-nav-text" tooltip>Introduction</span> </div></li> <li class="devsite-nav-item"><a href="/articles/what-are-pwas" class="devsite-nav-title gc-analytics-event" data-category="Site-Wide Custom Events" data-label="Book nav link, pathname: /articles/what-are-pwas" track-type="bookNav" track-name="click" track-metadata-eventdetail="/articles/what-are-pwas" ><span class="devsite-nav-text" tooltip>What are Progressive Web Apps?</span></a></li> <li class="devsite-nav-item"><a href="/articles/pwa-checklist" class="devsite-nav-title gc-analytics-event" data-category="Site-Wide Custom Events" data-label="Book nav link, pathname: /articles/pwa-checklist" track-type="bookNav" track-name="click" track-metadata-eventdetail="/articles/pwa-checklist" ><span class="devsite-nav-text" tooltip>What makes a good Progressive Web App?</span></a></li> <li class="devsite-nav-item devsite-nav-heading"><div class="devsite-nav-title devsite-nav-title-no-path"> <span class="devsite-nav-text" tooltip>Make it installable</span> </div></li> <li class="devsite-nav-item"><a href="/articles/install-criteria" class="devsite-nav-title gc-analytics-event" data-category="Site-Wide Custom Events" data-label="Book nav link, pathname: /articles/install-criteria" track-type="bookNav" track-name="click" track-metadata-eventdetail="/articles/install-criteria" ><span class="devsite-nav-text" tooltip>What does it take to be installable?</span></a></li> <li class="devsite-nav-item"><a href="/articles/customize-install" class="devsite-nav-title gc-analytics-event" data-category="Site-Wide Custom Events" data-label="Book nav link, pathname: /articles/customize-install" track-type="bookNav" track-name="click" track-metadata-eventdetail="/articles/customize-install" ><span class="devsite-nav-text" tooltip>How to provide your own in-app install experience</span></a></li> <li class="devsite-nav-item"><a href="/articles/define-install-strategy" class="devsite-nav-title gc-analytics-event" data-category="Site-Wide Custom Events" data-label="Book nav link, pathname: /articles/define-install-strategy" track-type="bookNav" track-name="click" track-metadata-eventdetail="/articles/define-install-strategy" ><span class="devsite-nav-text" tooltip>How to define your install strategy</span></a></li> <li class="devsite-nav-item devsite-nav-heading"><div class="devsite-nav-title devsite-nav-title-no-path"> <span class="devsite-nav-text" tooltip>Create an app-like user experience</span> </div></li> <li class="devsite-nav-item"><a href="/articles/app-like-pwas" class="devsite-nav-title gc-analytics-event" data-category="Site-Wide Custom Events" data-label="Book nav link, pathname: /articles/app-like-pwas" track-type="bookNav" track-name="click" track-metadata-eventdetail="/articles/app-like-pwas" ><span class="devsite-nav-text" tooltip>Make your PWA feel more like an app</span></a></li> <li class="devsite-nav-item"><a href="/articles/progressively-enhance-your-pwa" class="devsite-nav-title gc-analytics-event" data-category="Site-Wide Custom Events" data-label="Book nav link, pathname: /articles/progressively-enhance-your-pwa" track-type="bookNav" track-name="click" track-metadata-eventdetail="/articles/progressively-enhance-your-pwa" ><span class="devsite-nav-text" tooltip>Progressively enhance your Progressive Web App</span></a></li> <li class="devsite-nav-item devsite-nav-heading"><div class="devsite-nav-title devsite-nav-title-no-path"> <span class="devsite-nav-text" tooltip>Advanced topics</span> </div></li> <li class="devsite-nav-item"><a href="/articles/multi-origin-pwas" class="devsite-nav-title gc-analytics-event" data-category="Site-Wide Custom Events" data-label="Book nav link, pathname: /articles/multi-origin-pwas" track-type="bookNav" track-name="click" track-metadata-eventdetail="/articles/multi-origin-pwas" ><span class="devsite-nav-text" tooltip>Progressive Web Apps in multi-origin sites</span></a></li> <li class="devsite-nav-item"><a href="/articles/building-multiple-pwas-on-the-same-domain" class="devsite-nav-title gc-analytics-event" data-category="Site-Wide Custom Events" data-label="Book nav link, pathname: /articles/building-multiple-pwas-on-the-same-domain" track-type="bookNav" track-name="click" track-metadata-eventdetail="/articles/building-multiple-pwas-on-the-same-domain" ><span class="devsite-nav-text" tooltip>Building multiple Progressive Web Apps on the same domain</span></a></li> <li class="devsite-nav-item"><a href="/articles/offline-ux-design-guidelines" class="devsite-nav-title gc-analytics-event" data-category="Site-Wide Custom Events" data-label="Book nav link, pathname: /articles/offline-ux-design-guidelines" track-type="bookNav" track-name="click" track-metadata-eventdetail="/articles/offline-ux-design-guidelines" ><span class="devsite-nav-text" tooltip>Offline UX design guidelines</span></a></li> <li class="devsite-nav-item"><a href="/articles/offline-fallback-page" class="devsite-nav-title gc-analytics-event" data-category="Site-Wide Custom Events" data-label="Book nav link, pathname: /articles/offline-fallback-page" track-type="bookNav" track-name="click" track-metadata-eventdetail="/articles/offline-fallback-page" ><span class="devsite-nav-text" tooltip>Create an offline fallback page</span></a></li> <li class="devsite-nav-item"><a href="/articles/pwas-in-app-stores" class="devsite-nav-title gc-analytics-event" data-category="Site-Wide Custom Events" data-label="Book nav link, pathname: /articles/pwas-in-app-stores" track-type="bookNav" track-name="click" track-metadata-eventdetail="/articles/pwas-in-app-stores" ><span class="devsite-nav-text" tooltip>PWAs in App Stores</span></a></li> <li class="devsite-nav-item"><a href="/articles/manifest-updates" class="devsite-nav-title gc-analytics-event" data-category="Site-Wide Custom Events" data-label="Book nav link, pathname: /articles/manifest-updates" track-type="bookNav" track-name="click" track-metadata-eventdetail="/articles/manifest-updates" ><span class="devsite-nav-text" tooltip>How Chrome handles updates to the web app manifest</span></a></li> <li class="devsite-nav-item devsite-nav-heading"><div class="devsite-nav-title devsite-nav-title-no-path"> <span class="devsite-nav-text" tooltip>Case studies</span> </div></li> <li class="devsite-nav-item"><a href="/articles/drive-business-success" class="devsite-nav-title gc-analytics-event" data-category="Site-Wide Custom Events" data-label="Book nav link, pathname: /articles/drive-business-success" track-type="bookNav" track-name="click" track-metadata-eventdetail="/articles/drive-business-success" ><span class="devsite-nav-text" tooltip>How Progressive Web Apps can drive business success</span></a></li> <li class="devsite-nav-item"><a href="/case-studies/deprecating-excalidraw-electron" class="devsite-nav-title gc-analytics-event" data-category="Site-Wide Custom Events" data-label="Book nav link, pathname: /case-studies/deprecating-excalidraw-electron" track-type="bookNav" track-name="click" track-metadata-eventdetail="/case-studies/deprecating-excalidraw-electron" ><span class="devsite-nav-text" tooltip>Deprecating Excalidraw Electron in favor of the web version</span></a></li> <li class="devsite-nav-item"><a href="/case-studies/clipchamp" class="devsite-nav-title gc-analytics-event" data-category="Site-Wide Custom Events" data-label="Book nav link, pathname: /case-studies/clipchamp" track-type="bookNav" track-name="click" track-metadata-eventdetail="/case-studies/clipchamp" ><span class="devsite-nav-text" tooltip>Clipchamp's video editor PWA installs see a 97% monthly growth</span></a></li> <li class="devsite-nav-item"><a href="/case-studies/gravit-designer" class="devsite-nav-title gc-analytics-event" data-category="Site-Wide Custom Events" data-label="Book nav link, pathname: /case-studies/gravit-designer" track-type="bookNav" track-name="click" track-metadata-eventdetail="/case-studies/gravit-designer" ><span class="devsite-nav-text" tooltip>PWA users are 2.5x more likely to purchase Gravit Designer PRO</span></a></li> <li class="devsite-nav-item"><a href="/case-studies/jdid" class="devsite-nav-title gc-analytics-event" data-category="Site-Wide Custom Events" data-label="Book nav link, pathname: /case-studies/jdid" track-type="bookNav" track-name="click" track-metadata-eventdetail="/case-studies/jdid" ><span class="devsite-nav-text" tooltip>JD.ID improves their mobile conversion rate by 53% with caching strategies, installation, and push notifications </span></a></li> <li class="devsite-nav-item"><a href="/case-studies/rakuten-24" class="devsite-nav-title gc-analytics-event" data-category="Site-Wide Custom Events" data-label="Book nav link, pathname: /case-studies/rakuten-24" track-type="bookNav" track-name="click" track-metadata-eventdetail="/case-studies/rakuten-24" ><span class="devsite-nav-text" tooltip>Rakuten 24’s investment in PWA increases user retention by 450%</span></a></li> <li class="devsite-nav-item"><a href="/case-studies/zdf" class="devsite-nav-title gc-analytics-event" data-category="Site-Wide Custom Events" data-label="Book nav link, pathname: /case-studies/zdf" track-type="bookNav" track-name="click" track-metadata-eventdetail="/case-studies/zdf" ><span class="devsite-nav-text" tooltip>How ZDF created a video PWA with offline and dark mode</span></a></li> <li class="devsite-nav-item"><a href="/case-studies/goibibo" class="devsite-nav-title gc-analytics-event" data-category="Site-Wide Custom Events" data-label="Book nav link, pathname: /case-studies/goibibo" track-type="bookNav" track-name="click" track-metadata-eventdetail="/case-studies/goibibo" ><span class="devsite-nav-text" tooltip>How Goibibo's PWA improved conversions by 60%</span></a></li> <li class="devsite-nav-item"><a href="/articles/building-a-pwa-at-google-part-1" class="devsite-nav-title gc-analytics-event" data-category="Site-Wide Custom Events" data-label="Book nav link, pathname: /articles/building-a-pwa-at-google-part-1" track-type="bookNav" track-name="click" track-metadata-eventdetail="/articles/building-a-pwa-at-google-part-1" ><span class="devsite-nav-text" tooltip>Building a PWA at Google, part 1 </span></a></li> </ul> </div> </div> </nav> </devsite-book-nav> <section id="gc-wrapper"> <main role="main" class="devsite-main-content" has-book-nav has-sidebar > <div class="devsite-sidebar"> <div class="devsite-sidebar-content"> <devsite-toc class="devsite-nav" role="navigation" aria-label="On this page" depth="2" scrollbars ></devsite-toc> <devsite-recommendations-sidebar class="nocontent devsite-nav"> </devsite-recommendations-sidebar> </div> </div> <devsite-content> <article class="devsite-article"> <div class="devsite-article-meta nocontent" role="navigation"> <ul class="devsite-breadcrumb-list" aria-label="Breadcrumb"> <li class="devsite-breadcrumb-item "> <a href="https://web.dev/" class="devsite-breadcrumb-link gc-analytics-event" data-category="Site-Wide Custom Events" data-label="Breadcrumbs" data-value="1" track-type="globalNav" track-name="breadcrumb" track-metadata-position="1" track-metadata-eventdetail="web.dev" > web.dev </a> </li> <li class="devsite-breadcrumb-item "> <div class="devsite-breadcrumb-guillemet material-icons" aria-hidden="true"></div> <a href="https://web.dev/explore" class="devsite-breadcrumb-link gc-analytics-event" data-category="Site-Wide Custom Events" data-label="Breadcrumbs" data-value="2" track-type="globalNav" track-name="breadcrumb" track-metadata-position="2" track-metadata-eventdetail="" > Explore </a> </li> <li class="devsite-breadcrumb-item "> <div class="devsite-breadcrumb-guillemet material-icons" aria-hidden="true"></div> <a href="https://web.dev/explore/progressive-web-apps" class="devsite-breadcrumb-link gc-analytics-event" data-category="Site-Wide Custom Events" data-label="Breadcrumbs" data-value="3" track-type="globalNav" track-name="breadcrumb" track-metadata-position="3" track-metadata-eventdetail="" > Progressive Web Apps </a> </li> </ul> <devsite-thumb-rating position="header"> </devsite-thumb-rating> </div> <h1 class="devsite-page-title" tabindex="-1"> How ZDF created a video PWA with offline and dark mode </h1> <devsite-feature-tooltip ack-key="AckCollectionsBookmarkTooltipDismiss" analytics-category="Site-Wide Custom Events" analytics-action-show="Callout Profile displayed" analytics-action-close="Callout Profile dismissed" analytics-label="Create Collection Callout" class="devsite-page-bookmark-tooltip nocontent" dismiss-button="true" id="devsite-collections-dropdown" dismiss-button-text="Dismiss" close-button-text="Got it"> <devsite-bookmark></devsite-bookmark> <span slot="popout-heading"> Stay organized with collections </span> <span slot="popout-contents"> Save and categorize content based on your preferences. </span> </devsite-feature-tooltip> <div class="devsite-page-title-meta"><devsite-view-release-notes></devsite-view-release-notes></div> <devsite-toc class="devsite-nav" depth="2" devsite-toc-embedded > </devsite-toc> <div class="devsite-article-body clearfix "> <p>Learn how ZDF created a progressive web app (PWA) with modern features like offline support, installability, and dark mode.</p> <p> <div class="wd-authors" translate="no"> <div class="wd-author"> <img class="devsite-landing-row-item-icon" alt="Martin Schierle" src="https://web.dev/images/authors/martinschierle.jpg" decoding="async" height="64" loading="lazy" width="64"> <div> <span> Martin Schierle </span> <div class="wd-author__links"> <a href="https://twitter.com/martinschierle" aria-label="Martin Schierle on X" rel="me"> <svg xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 300 271"> <title>X</title> <path fill="currentColor" d="m236 0h46l-101 115 118 156h-92.6l-72.5-94.8-83 94.8h-46l107-123-113-148h94.9l65.5 86.6zm-16.1 244h25.5l-165-218h-27.4z"></path> </svg></a> <a href="https://github.com/martinschierle" aria-label="Martin Schierle on GitHub" rel="me"> <svg xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 32.6 31.8"> <title>GitHub</title> <path d="M16.3 0C7.3 0 0 7.3 0 16.3c0 7.2 4.7 13.3 11.1 15.5.8.1 1.1-.4 1.1-.8v-2.8c-4.5 1-5.5-2.2-5.5-2.2-.7-1.9-1.8-2.4-1.8-2.4-1.5-1 .1-1 .1-1 1.6.1 2.5 1.7 2.5 1.7 1.5 2.5 3.8 1.8 4.7 1.4.1-1.1.6-1.8 1-2.2-3.6-.4-7.4-1.8-7.4-8.1 0-1.8.6-3.2 1.7-4.4-.1-.3-.7-2 .2-4.2 0 0 1.4-.4 4.5 1.7 1.3-.4 2.7-.5 4.1-.5 1.4 0 2.8.2 4.1.5 3.1-2.1 4.5-1.7 4.5-1.7.9 2.2.3 3.9.2 4.3 1 1.1 1.7 2.6 1.7 4.4 0 6.3-3.8 7.6-7.4 8 .6.5 1.1 1.5 1.1 3V31c0 .4.3.9 1.1.8 6.5-2.2 11.1-8.3 11.1-15.5C32.6 7.3 25.3 0 16.3 0z" fill-rule="evenodd" clip-rule="evenodd" fill="currentColor" /> </svg></a> </div> </div> </div> <div class="wd-author"> <img class="devsite-landing-row-item-icon" alt="Scott Friesen" src="https://web.dev/images/authors/scottfriesen.jpg" decoding="async" height="64" loading="lazy" width="64"> <div> <span> Scott Friesen </span> <div class="wd-author__links"> <a href="https://github.com/scott-friesen" aria-label="Scott Friesen on GitHub" rel="me"> <svg xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 32.6 31.8"> <title>GitHub</title> <path d="M16.3 0C7.3 0 0 7.3 0 16.3c0 7.2 4.7 13.3 11.1 15.5.8.1 1.1-.4 1.1-.8v-2.8c-4.5 1-5.5-2.2-5.5-2.2-.7-1.9-1.8-2.4-1.8-2.4-1.5-1 .1-1 .1-1 1.6.1 2.5 1.7 2.5 1.7 1.5 2.5 3.8 1.8 4.7 1.4.1-1.1.6-1.8 1-2.2-3.6-.4-7.4-1.8-7.4-8.1 0-1.8.6-3.2 1.7-4.4-.1-.3-.7-2 .2-4.2 0 0 1.4-.4 4.5 1.7 1.3-.4 2.7-.5 4.1-.5 1.4 0 2.8.2 4.1.5 3.1-2.1 4.5-1.7 4.5-1.7.9 2.2.3 3.9.2 4.3 1 1.1 1.7 2.6 1.7 4.4 0 6.3-3.8 7.6-7.4 8 .6.5 1.1 1.5 1.1 3V31c0 .4.3.9 1.1.8 6.5-2.2 11.1-8.3 11.1-15.5C32.6 7.3 25.3 0 16.3 0z" fill-rule="evenodd" clip-rule="evenodd" fill="currentColor" /> </svg></a> </div> </div> </div> </div></p> <p>When broadcaster ZDF was considering redesigning their frontend technology stack, they decided to take a closer look at <a href="/pwa">Progressive Web Apps</a> for their streaming site <a href="https://pwa.zdf.de/">ZDFmediathek</a>. Development agency <a href="https://www.cellular.de/">Cellular</a> 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.</p> <h2 id="adding_a_service_worker" data-text="Adding a service worker" tabindex="-1">Adding a service worker</h2> <p>A key feature of a PWA is offline support. For ZDF most of the heavy lifting is done by <a href="/case-studies/aem-with-workbox">Workbox</a>, 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 <a href="https://reactjs.org/docs/create-a-new-react-app.html">create-react-app</a> to precache static assets. This lets the application focus on making the dynamic content available offline, in this case the videos and their metadata.</p> <p>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.</p> <p>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.</p> <p>This is where one of the web's most powerful features comes to the rescue: <a href="/articles/service-worker-mindset">service workers</a>.</p> <p>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.</p> <p>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 <a href="/articles/storage-for-the-web#how-much">StorageManager API</a> 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 <a href="https://cellular.github.io/quota">small utility</a> to test the behavior on various devices. By now a <a href="/articles/storage-for-the-web">comprehensive article</a> exists that sums up all the details.</p> <h2 id="adding_a_custom_install_prompt" data-text="Adding a custom install prompt" tabindex="-1">Adding a custom install prompt</h2> <p>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.</p> <figure> <div class="wd-switcher"> <img src="/static/case-studies/zdf/image/custom-invitation-instal-10ab87790d8bb.jpg" alt="Custom invitation to install." width="800" height="1595" srcset="https://web.dev/static/case-studies/zdf/image/custom-invitation-instal-10ab87790d8bb_36.jpg 36w,https://web.dev/static/case-studies/zdf/image/custom-invitation-instal-10ab87790d8bb_48.jpg 48w,https://web.dev/static/case-studies/zdf/image/custom-invitation-instal-10ab87790d8bb_72.jpg 72w,https://web.dev/static/case-studies/zdf/image/custom-invitation-instal-10ab87790d8bb_96.jpg 96w,https://web.dev/static/case-studies/zdf/image/custom-invitation-instal-10ab87790d8bb_480.jpg 480w,https://web.dev/static/case-studies/zdf/image/custom-invitation-instal-10ab87790d8bb_720.jpg 720w,https://web.dev/static/case-studies/zdf/image/custom-invitation-instal-10ab87790d8bb_856.jpg 856w,https://web.dev/static/case-studies/zdf/image/custom-invitation-instal-10ab87790d8bb_960.jpg 960w,https://web.dev/static/case-studies/zdf/image/custom-invitation-instal-10ab87790d8bb_1440.jpg 1440w,https://web.dev/static/case-studies/zdf/image/custom-invitation-instal-10ab87790d8bb_1920.jpg 1920w,https://web.dev/static/case-studies/zdf/image/custom-invitation-instal-10ab87790d8bb_2880.jpg 2880w" sizes="(max-width: 840px) 100vw, 856px"> <img src="/static/case-studies/zdf/image/custom-install-prompt-bei-fcf49ed4703b6.jpg" alt="Custom install prompt being triggered when downloading a video for offline consumption." width="800" height="1595" srcset="https://web.dev/static/case-studies/zdf/image/custom-install-prompt-bei-fcf49ed4703b6_36.jpg 36w,https://web.dev/static/case-studies/zdf/image/custom-install-prompt-bei-fcf49ed4703b6_48.jpg 48w,https://web.dev/static/case-studies/zdf/image/custom-install-prompt-bei-fcf49ed4703b6_72.jpg 72w,https://web.dev/static/case-studies/zdf/image/custom-install-prompt-bei-fcf49ed4703b6_96.jpg 96w,https://web.dev/static/case-studies/zdf/image/custom-install-prompt-bei-fcf49ed4703b6_480.jpg 480w,https://web.dev/static/case-studies/zdf/image/custom-install-prompt-bei-fcf49ed4703b6_720.jpg 720w,https://web.dev/static/case-studies/zdf/image/custom-install-prompt-bei-fcf49ed4703b6_856.jpg 856w,https://web.dev/static/case-studies/zdf/image/custom-install-prompt-bei-fcf49ed4703b6_960.jpg 960w,https://web.dev/static/case-studies/zdf/image/custom-install-prompt-bei-fcf49ed4703b6_1440.jpg 1440w,https://web.dev/static/case-studies/zdf/image/custom-install-prompt-bei-fcf49ed4703b6_1920.jpg 1920w,https://web.dev/static/case-studies/zdf/image/custom-install-prompt-bei-fcf49ed4703b6_2880.jpg 2880w" sizes="(max-width: 840px) 100vw, 856px"> </div> <figcaption class="wd-caption">Custom install prompt being triggered when downloading a video for offline consumption.</figcaption> </figure> <h2 id="building_an_offline_page_to_access_downloads" data-text="Building an offline page to access downloads" tabindex="-1">Building an offline page to access downloads</h2> <p>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.</p> <figure> <div class="wd-switcher"> <img src="/static/case-studies/zdf/image/offline-page-showing-con-97273fdc6db79.png" alt="Offline page showing all content available for watching offline." width="800" height="1418" srcset="https://web.dev/static/case-studies/zdf/image/offline-page-showing-con-97273fdc6db79_36.png 36w,https://web.dev/static/case-studies/zdf/image/offline-page-showing-con-97273fdc6db79_48.png 48w,https://web.dev/static/case-studies/zdf/image/offline-page-showing-con-97273fdc6db79_72.png 72w,https://web.dev/static/case-studies/zdf/image/offline-page-showing-con-97273fdc6db79_96.png 96w,https://web.dev/static/case-studies/zdf/image/offline-page-showing-con-97273fdc6db79_480.png 480w,https://web.dev/static/case-studies/zdf/image/offline-page-showing-con-97273fdc6db79_720.png 720w,https://web.dev/static/case-studies/zdf/image/offline-page-showing-con-97273fdc6db79_856.png 856w,https://web.dev/static/case-studies/zdf/image/offline-page-showing-con-97273fdc6db79_960.png 960w,https://web.dev/static/case-studies/zdf/image/offline-page-showing-con-97273fdc6db79_1440.png 1440w,https://web.dev/static/case-studies/zdf/image/offline-page-showing-con-97273fdc6db79_1920.png 1920w,https://web.dev/static/case-studies/zdf/image/offline-page-showing-con-97273fdc6db79_2880.png 2880w" sizes="(max-width: 840px) 100vw, 856px"> <img src="/static/case-studies/zdf/image/offline-page-showing-no-8a1022d8134c8.png" alt="Offline page showing that no content is available for watching offline." width="800" height="1423" srcset="https://web.dev/static/case-studies/zdf/image/offline-page-showing-no-8a1022d8134c8_36.png 36w,https://web.dev/static/case-studies/zdf/image/offline-page-showing-no-8a1022d8134c8_48.png 48w,https://web.dev/static/case-studies/zdf/image/offline-page-showing-no-8a1022d8134c8_72.png 72w,https://web.dev/static/case-studies/zdf/image/offline-page-showing-no-8a1022d8134c8_96.png 96w,https://web.dev/static/case-studies/zdf/image/offline-page-showing-no-8a1022d8134c8_480.png 480w,https://web.dev/static/case-studies/zdf/image/offline-page-showing-no-8a1022d8134c8_720.png 720w,https://web.dev/static/case-studies/zdf/image/offline-page-showing-no-8a1022d8134c8_856.png 856w,https://web.dev/static/case-studies/zdf/image/offline-page-showing-no-8a1022d8134c8_960.png 960w,https://web.dev/static/case-studies/zdf/image/offline-page-showing-no-8a1022d8134c8_1440.png 1440w,https://web.dev/static/case-studies/zdf/image/offline-page-showing-no-8a1022d8134c8_1920.png 1920w,https://web.dev/static/case-studies/zdf/image/offline-page-showing-no-8a1022d8134c8_2880.png 2880w" sizes="(max-width: 840px) 100vw, 856px"> </div> <figcaption class="wd-caption">Offline page showing all content available for watching offline.</figcaption> </figure> <h2 id="using_frame_loading_rate_for_adaptive_features" data-text="Using frame loading rate for adaptive features" tabindex="-1">Using frame loading rate for adaptive features</h2> <p>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 <code translate="no" dir="ltr">requestAnimationFrame()</code> while the application loads and disables all animations when the value drops below a certain threshold.</p> <div></div><devsite-code><pre class="devsite-click-to-copy" translate="no" dir="ltr" is-upgraded syntax="JavaScript"><code translate="no" dir="ltr"><span class="devsite-syntax-kd">const</span><span class="devsite-syntax-w"> </span><span class="devsite-syntax-nx">frameRate</span><span class="devsite-syntax-w"> </span><span class="devsite-syntax-o">=</span><span class="devsite-syntax-w"> </span><span class="devsite-syntax-ow">new</span><span class="devsite-syntax-w"> </span><span class="devsite-syntax-nb">Promise</span><span class="devsite-syntax-p">(</span><span class="devsite-syntax-nx">resolve</span><span class="devsite-syntax-w"> </span><span class="devsite-syntax-o">=</span>><span class="devsite-syntax-w"> </span><span class="devsite-syntax-p">{</span> <span class="devsite-syntax-w"> </span><span class="devsite-syntax-kd">let</span><span class="devsite-syntax-w"> </span><span class="devsite-syntax-nx">lastTick</span><span class="devsite-syntax-p">;</span> <span class="devsite-syntax-w"> </span><span class="devsite-syntax-kd">const</span><span class="devsite-syntax-w"> </span><span class="devsite-syntax-nx">samples</span><span class="devsite-syntax-w"> </span><span class="devsite-syntax-o">=</span><span class="devsite-syntax-w"> </span><span class="devsite-syntax-p">[];</span> <span class="devsite-syntax-w"> </span><span class="devsite-syntax-kd">function</span><span class="devsite-syntax-w"> </span><span class="devsite-syntax-nx">measure</span><span class="devsite-syntax-p">()</span><span class="devsite-syntax-w"> </span><span class="devsite-syntax-p">{</span> <span class="devsite-syntax-w"> </span><span class="devsite-syntax-kd">const</span><span class="devsite-syntax-w"> </span><span class="devsite-syntax-nx">tick</span><span class="devsite-syntax-w"> </span><span class="devsite-syntax-o">=</span><span class="devsite-syntax-w"> </span><span class="devsite-syntax-nb">Date</span><span class="devsite-syntax-p">.</span><span class="devsite-syntax-nx">now</span><span class="devsite-syntax-p">();</span> <span class="devsite-syntax-w"> </span><span class="devsite-syntax-k">if</span><span class="devsite-syntax-w"> </span><span class="devsite-syntax-p">(</span><span class="devsite-syntax-nx">lastTick</span><span class="devsite-syntax-p">)</span><span class="devsite-syntax-w"> </span><span class="devsite-syntax-nx">samples</span><span class="devsite-syntax-p">.</span><span class="devsite-syntax-nx">push</span><span class="devsite-syntax-p">(</span><span class="devsite-syntax-nx">tick</span><span class="devsite-syntax-w"> </span><span class="devsite-syntax-o">-</span><span class="devsite-syntax-w"> </span><span class="devsite-syntax-nx">lastTick</span><span class="devsite-syntax-p">);</span> <span class="devsite-syntax-w"> </span><span class="devsite-syntax-nx">lastTick</span><span class="devsite-syntax-w"> </span><span class="devsite-syntax-o">=</span><span class="devsite-syntax-w"> </span><span class="devsite-syntax-nx">tick</span><span class="devsite-syntax-p">;</span> <span class="devsite-syntax-w"> </span><span class="devsite-syntax-k">if</span><span class="devsite-syntax-w"> </span><span class="devsite-syntax-p">(</span><span class="devsite-syntax-nx">samples</span><span class="devsite-syntax-p">.</span><span class="devsite-syntax-nx">length</span><span class="devsite-syntax-w"> < </span><span class="devsite-syntax-mf">20</span><span class="devsite-syntax-p">)</span><span class="devsite-syntax-w"> </span><span class="devsite-syntax-nx">requestAnimationFrame</span><span class="devsite-syntax-p">(</span><span class="devsite-syntax-nx">measure</span><span class="devsite-syntax-p">);</span> <span class="devsite-syntax-w"> </span><span class="devsite-syntax-k">else</span><span class="devsite-syntax-w"> </span><span class="devsite-syntax-p">{</span> <span class="devsite-syntax-w"> </span><span class="devsite-syntax-kd">const</span><span class="devsite-syntax-w"> </span><span class="devsite-syntax-nx">avg</span><span class="devsite-syntax-w"> </span><span class="devsite-syntax-o">=</span><span class="devsite-syntax-w"> </span><span class="devsite-syntax-nx">samples</span><span class="devsite-syntax-p">.</span><span class="devsite-syntax-nx">reduce</span><span class="devsite-syntax-p">((</span><span class="devsite-syntax-nx">a</span><span class="devsite-syntax-p">,</span><span class="devsite-syntax-w"> </span><span class="devsite-syntax-nx">b</span><span class="devsite-syntax-p">)</span><span class="devsite-syntax-w"> </span><span class="devsite-syntax-o">=</span>><span class="devsite-syntax-w"> </span><span class="devsite-syntax-nx">a</span><span class="devsite-syntax-w"> </span><span class="devsite-syntax-o">+</span><span class="devsite-syntax-w"> </span><span class="devsite-syntax-nx">b</span><span class="devsite-syntax-p">)</span><span class="devsite-syntax-w"> </span><span class="devsite-syntax-o">/</span><span class="devsite-syntax-w"> </span><span class="devsite-syntax-nx">samples</span><span class="devsite-syntax-p">.</span><span class="devsite-syntax-nx">length</span><span class="devsite-syntax-p">;</span> <span class="devsite-syntax-w"> </span><span class="devsite-syntax-kd">const</span><span class="devsite-syntax-w"> </span><span class="devsite-syntax-nx">fps</span><span class="devsite-syntax-w"> </span><span class="devsite-syntax-o">=</span><span class="devsite-syntax-w"> </span><span class="devsite-syntax-mf">1000</span><span class="devsite-syntax-w"> </span><span class="devsite-syntax-o">/</span><span class="devsite-syntax-w"> </span><span class="devsite-syntax-nx">avg</span><span class="devsite-syntax-p">;</span> <span class="devsite-syntax-w"> </span><span class="devsite-syntax-nx">resolve</span><span class="devsite-syntax-p">(</span><span class="devsite-syntax-nx">fps</span><span class="devsite-syntax-p">);</span> <span class="devsite-syntax-w"> </span><span class="devsite-syntax-p">}</span> <span class="devsite-syntax-w"> </span><span class="devsite-syntax-p">}</span> <span class="devsite-syntax-w"> </span><span class="devsite-syntax-nx">measure</span><span class="devsite-syntax-p">();</span> <span class="devsite-syntax-p">});</span> </code></pre></devsite-code> <p>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 <a href="/case-studies/adaptive-loading-with-service-workers">other techniques for adaptive loading</a> that developers can implement. One great advantage of this approach is that it is available on all platforms.</p> <h2 id="dark_mode" data-text="Dark mode" tabindex="-1">Dark mode</h2> <p>A popular feature for modern mobile experiences is <a href="/articles/prefers-color-scheme">dark mode</a>. 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.</p> <h2 id="results" data-text="Results" tabindex="-1">Results</h2> <p>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.</p> <h2 id="whats_next" data-text="What's next?" tabindex="-1">What's next?</h2> <p>ZDF plans to continue adding features to their PWA, including login for personalization, cross-device and platform viewing, and push notifications.</p> </div> <devsite-thumb-rating position="footer"> </devsite-thumb-rating> <div class="devsite-floating-action-buttons"> </div> </article> <devsite-content-footer class="nocontent"> <p>Except as otherwise noted, the content of this page is licensed under the <a href="https://creativecommons.org/licenses/by/4.0/">Creative Commons Attribution 4.0 License</a>, and code samples are licensed under the <a href="https://www.apache.org/licenses/LICENSE-2.0">Apache 2.0 License</a>. For details, see the <a href="https://developers.google.com/site-policies">Google Developers Site Policies</a>. Java is a registered trademark of Oracle and/or its affiliates.</p> <p>Last updated 2020-10-07 UTC.</p> </devsite-content-footer> <devsite-notification > </devsite-notification> <div class="devsite-content-data"> <template class="devsite-content-data-template"> [[["Easy to understand","easyToUnderstand","thumb-up"],["Solved my problem","solvedMyProblem","thumb-up"],["Other","otherUp","thumb-up"]],[["Missing the information I need","missingTheInformationINeed","thumb-down"],["Too complicated / too many steps","tooComplicatedTooManySteps","thumb-down"],["Out of date","outOfDate","thumb-down"],["Samples / code issue","samplesCodeIssue","thumb-down"],["Other","otherDown","thumb-down"]],["Last updated 2020-10-07 UTC."],[],[]] </template> </div> </devsite-content> </main> <devsite-footer-promos class="devsite-footer"> </devsite-footer-promos> <devsite-footer-linkboxes class="devsite-footer"> <nav class="devsite-footer-linkboxes nocontent" aria-label="Footer links"> <ul class="devsite-footer-linkboxes-list"> <li class="devsite-footer-linkbox wd-footer-promo"> <h3 class="devsite-footer-linkbox-heading no-link">web.dev</h3> <ul class="devsite-footer-linkbox-list"> <li class="devsite-footer-linkbox-item"> <h3 class="devsite-footer-linkbox-heading no-link"> web.dev </h3> <div class="devsite-footer-linkbox-description">We want to help you build beautiful, accessible, fast, and secure websites that work cross-browser, and for all of your users. This site is our home for content to help you on that journey, written by members of the Chrome team, and external experts.</div> </li> </ul> </li> <li class="devsite-footer-linkbox "> <h3 class="devsite-footer-linkbox-heading no-link">Contribute</h3> <ul class="devsite-footer-linkbox-list"> <li class="devsite-footer-linkbox-item"> <a href="https://issuetracker.google.com/issues/new?component=1400680&template=1857359" class="devsite-footer-linkbox-link gc-analytics-event" data-category="Site-Wide Custom Events" data-label="Footer Link (index 1)" > File a bug </a> </li> <li class="devsite-footer-linkbox-item"> <a href="https://issuetracker.google.com/issues?q=status:open%20componentid:1400680&s=created_time:desc" class="devsite-footer-linkbox-link gc-analytics-event" data-category="Site-Wide Custom Events" data-label="Footer Link (index 2)" > See open issues </a> </li> </ul> </li> <li class="devsite-footer-linkbox "> <h3 class="devsite-footer-linkbox-heading no-link">Related Content</h3> <ul class="devsite-footer-linkbox-list"> <li class="devsite-footer-linkbox-item"> <a href="https://developer.chrome.com/" class="devsite-footer-linkbox-link gc-analytics-event" data-category="Site-Wide Custom Events" data-label="Footer Link (index 1)" > Chrome for Developers </a> </li> <li class="devsite-footer-linkbox-item"> <a href="https://blog.chromium.org/" class="devsite-footer-linkbox-link gc-analytics-event" data-category="Site-Wide Custom Events" data-label="Footer Link (index 2)" > Chromium updates </a> </li> <li class="devsite-footer-linkbox-item"> <a href="/case-studies" class="devsite-footer-linkbox-link gc-analytics-event" data-category="Site-Wide Custom Events" data-label="Footer Link (index 3)" > Case studies </a> </li> <li class="devsite-footer-linkbox-item"> <a href="/shows" class="devsite-footer-linkbox-link gc-analytics-event" data-category="Site-Wide Custom Events" data-label="Footer Link (index 4)" > Podcasts & shows </a> </li> </ul> </li> <li class="devsite-footer-linkbox "> <h3 class="devsite-footer-linkbox-heading no-link">Follow</h3> <ul class="devsite-footer-linkbox-list"> <li class="devsite-footer-linkbox-item"> <a href="https://twitter.com/ChromiumDev" class="devsite-footer-linkbox-link gc-analytics-event" data-category="Site-Wide Custom Events" data-label="Footer Link (index 1)" > @ChromiumDev on X </a> </li> <li class="devsite-footer-linkbox-item"> <a href="https://www.youtube.com/user/ChromeDevelopers" class="devsite-footer-linkbox-link gc-analytics-event" data-category="Site-Wide Custom Events" data-label="Footer Link (index 2)" > YouTube </a> </li> <li class="devsite-footer-linkbox-item"> <a href="https://www.linkedin.com/showcase/chrome-for-developers" class="devsite-footer-linkbox-link gc-analytics-event" data-category="Site-Wide Custom Events" data-label="Footer Link (index 3)" > Chrome for Developers on LinkedIn </a> </li> <li class="devsite-footer-linkbox-item"> <a href="/static/blog/feed.xml" class="devsite-footer-linkbox-link gc-analytics-event" data-category="Site-Wide Custom Events" data-label="Footer Link (index 4)" > RSS </a> </li> </ul> </li> </ul> </nav> </devsite-footer-linkboxes> <devsite-footer-utility class="devsite-footer"> <div class="devsite-footer-utility nocontent"> <nav class="devsite-footer-utility-links" aria-label="Utility links"> <ul class="devsite-footer-utility-list"> <li class="devsite-footer-utility-item "> <a class="devsite-footer-utility-link gc-analytics-event" href="//policies.google.com/terms" data-category="Site-Wide Custom Events" data-label="Footer Terms link" > Terms </a> </li> <li class="devsite-footer-utility-item "> <a class="devsite-footer-utility-link gc-analytics-event" href="//policies.google.com/privacy" data-category="Site-Wide Custom Events" data-label="Footer Privacy link" > Privacy </a> </li> <li class="devsite-footer-utility-item glue-cookie-notification-bar-control"> <a class="devsite-footer-utility-link gc-analytics-event" href="#" data-category="Site-Wide Custom Events" data-label="Footer Manage cookies link" aria-hidden="true" > Manage cookies </a> </li> </ul> <devsite-language-selector> <ul role="presentation"> <li role="presentation"> <a role="menuitem" lang="en" >English</a> </li> <li role="presentation"> <a role="menuitem" lang="de" >Deutsch</a> </li> <li role="presentation"> <a role="menuitem" lang="es_419" >Español – América Latina</a> </li> <li role="presentation"> <a role="menuitem" lang="fr" >Français</a> </li> <li role="presentation"> <a role="menuitem" lang="id" >Indonesia</a> </li> <li role="presentation"> <a role="menuitem" lang="it" >Italiano</a> </li> <li role="presentation"> <a role="menuitem" lang="pl" >Polski</a> </li> <li role="presentation"> <a role="menuitem" lang="pt_br" >Português – Brasil</a> </li> <li role="presentation"> <a role="menuitem" lang="vi" >Tiếng Việt</a> </li> <li role="presentation"> <a role="menuitem" lang="tr" >Türkçe</a> </li> <li role="presentation"> <a role="menuitem" lang="ru" >Русский</a> </li> <li role="presentation"> <a role="menuitem" lang="he" >עברית</a> </li> <li role="presentation"> <a role="menuitem" lang="ar" >العربيّة</a> </li> <li role="presentation"> <a role="menuitem" lang="fa" >فارسی</a> </li> <li role="presentation"> <a role="menuitem" lang="hi" >हिंदी</a> </li> <li role="presentation"> <a role="menuitem" lang="bn" >বাংলা</a> </li> <li role="presentation"> <a role="menuitem" lang="th" >ภาษาไทย</a> </li> <li role="presentation"> <a role="menuitem" lang="zh_cn" >中文 – 简体</a> </li> <li role="presentation"> <a role="menuitem" lang="zh_tw" >中文 – 繁體</a> </li> <li role="presentation"> <a role="menuitem" lang="ja" >日本語</a> </li> <li role="presentation"> <a role="menuitem" lang="ko" >한국어</a> </li> </ul> </devsite-language-selector> </nav> </div> </devsite-footer-utility> <devsite-panel></devsite-panel> </section></section> <devsite-sitemask></devsite-sitemask> <devsite-snackbar></devsite-snackbar> <devsite-tooltip ></devsite-tooltip> <devsite-heading-link></devsite-heading-link> <devsite-analytics> <script type="application/json" analytics>[]</script> <script type="application/json" tag-management>{"at": "True", "ga4": [], "ga4p": [], "gtm": [{"id": "GTM-MZWCJPP", "purpose": 0}], "parameters": {"internalUser": "False", "language": {"machineTranslated": "False", "requested": "en", "served": "en"}, "pageType": "case-study", "projectName": "web.dev", "signedIn": "False", "tenant": "web", "recommendations": {"sourcePage": "", "sourceType": 0, "sourceRank": 0, "sourceIdenticalDescriptions": 0, "sourceTitleWords": 0, "sourceDescriptionWords": 0, "experiment": ""}, "experiment": {"ids": ""}}}</script> </devsite-analytics> <devsite-badger></devsite-badger> <script nonce="T19Y1T1+LhIfr7b35aFmmlZXH99KX+"> (function(d,e,v,s,i,t,E){d['GoogleDevelopersObject']=i; t=e.createElement(v);t.async=1;t.src=s;E=e.getElementsByTagName(v)[0]; E.parentNode.insertBefore(t,E);})(window, document, 'script', 'https://www.gstatic.com/devrel-devsite/prod/v870e399c64f7c43c99a3043db4b3a74327bb93d0914e84a0c3dba90bbfd67625/web/js/app_loader.js', '[27,"en",null,"/js/devsite_app_module.js","https://www.gstatic.com/devrel-devsite/prod/v870e399c64f7c43c99a3043db4b3a74327bb93d0914e84a0c3dba90bbfd67625","https://www.gstatic.com/devrel-devsite/prod/v870e399c64f7c43c99a3043db4b3a74327bb93d0914e84a0c3dba90bbfd67625/web","https://web-dot-devsite-v2-prod-3p.appspot.com",1,null,["/_pwa/web/manifest.json","https://www.gstatic.com/devrel-devsite/prod/v870e399c64f7c43c99a3043db4b3a74327bb93d0914e84a0c3dba90bbfd67625/images/video-placeholder.svg","https://www.gstatic.com/devrel-devsite/prod/v870e399c64f7c43c99a3043db4b3a74327bb93d0914e84a0c3dba90bbfd67625/web/images/favicon.png","https://www.gstatic.com/devrel-devsite/prod/v870e399c64f7c43c99a3043db4b3a74327bb93d0914e84a0c3dba90bbfd67625/web/images/lockup.svg","https://fonts.googleapis.com/css?family=Google+Sans:400,500|Roboto:400,400italic,500,500italic,700,700italic|Roboto+Mono:400,500,700&display=swap"],1,null,[1,6,8,12,14,17,21,25,50,52,63,70,75,76,80,87,91,92,93,97,98,100,101,102,103,104,105,107,108,109,110,112,113,116,117,118,120,122,124,125,126,127,129,130,131,132,133,134,135,136,138,140,141,147,148,149,151,152,156,157,158,159,161,163,164,168,169,170,179,180,182,183,186,191,193,196],"AIzaSyCNm9YxQumEXwGJgTDjxoxXK6m1F-9720Q","AIzaSyCc76DZePGtoyUjqKrLdsMGk_ry7sljLbY","web.dev","AIzaSyB9bqgQ2t11WJsOX8qNsCQ6U-w91mmqF-I","AIzaSyAdYnStPdzjcJJtQ0mvIaeaMKj7_t6J_Fg",null,null,null,["Cloud__enable_cloud_facet_chat","Search__enable_dynamic_content_confidential_banner","TpcFeatures__enable_mirror_tenant_redirects","Cloud__enable_cloudx_ping","BookNav__enable_tenant_cache_key","Cloud__enable_free_trial_server_call","CloudShell__cloud_code_overflow_menu","Profiles__enable_dashboard_curated_recommendations","EngEduTelemetry__enable_engedu_telemetry","Profiles__enable_complete_playlist_endpoint","MiscFeatureFlags__developers_footer_dark_image","MiscFeatureFlags__enable_view_transitions","Profiles__enable_awarding_url","Cloud__enable_cloud_dlp_service","Profiles__enable_page_saving","Cloud__enable_legacy_calculator_redirect","TpcFeatures__enable_required_headers","Profiles__enable_completecodelab_endpoint","Profiles__enable_developer_profiles_callout","OnSwitch__enable","Analytics__enable_clearcut_logging","Search__enable_suggestions_from_borg","Search__enable_page_map","Profiles__enable_profile_collections","Profiles__enable_public_developer_profiles","Cloud__enable_llm_concierge_chat","Profiles__enable_recognition_badges","MiscFeatureFlags__enable_project_variables","DevPro__enable_developer_subscriptions","Cloud__enable_cloud_shell_fte_user_flow","Cloud__enable_cloudx_experiment_ids","Profiles__enable_release_notes_notifications","MiscFeatureFlags__enable_firebase_utm","Search__enable_ai_eligibility_checks","Profiles__require_profile_eligibility_for_signin","Concierge__enable_pushui","MiscFeatureFlags__enable_variable_operator","MiscFeatureFlags__enable_explain_this_code","MiscFeatureFlags__developers_footer_image","Experiments__reqs_query_experiments","Cloud__enable_cloud_shell","CloudShell__cloud_shell_button","MiscFeatureFlags__emergency_css","DevPro__enable_cloud_innovators_plus"],null,null,"AIzaSyA58TaKli1DculwmAmbpzLVGuWc8eCQgQc","https://developerscontentserving-pa.googleapis.com","AIzaSyDWBU60w0P9hEkr29kkksYs8Z7gvZ8u_wc","https://developerscontentsearch-pa.googleapis.com",2,4,null,"https://developerprofiles-pa.googleapis.com",[27,"web","web.dev","web.dev",null,"web-dot-devsite-v2-prod-3p.appspot.com",null,null,[null,null,null,null,null,null,null,null,null,null,null,[1],null,null,null,null,null,null,[1],null,null,null,null,[1,null,1],[1,1,null,1,1]],null,[38,null,null,null,null,null,"/images/lockup.svg","/images/touchicon-180.png",null,null,null,1,1,null,null,null,null,null,null,null,null,2,null,null,null,"/images/lockup-dark-theme.svg",[]],[],null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,[[],[1,1]],[[null,null,null,null,null,["GTM-MZWCJPP"],null,null,null,null,null,[["GTM-MZWCJPP",1]],1]],null,4]]') </script> <devsite-a11y-announce></devsite-a11y-announce> </body> </html>