Skip to content

Web App Manifest Members Reference

This page documents every member you can put in a web app manifest: the core members of the W3C specification, the store metadata members from the Application Information Note, the Chromium incubations that ship today, vendor-specific members, and the notable proposals that have not shipped. For each member you get its JSON type, valid values, default, the exact processing rules (including what browsers do with invalid input), which browsers act on it, a JSON example and the gotchas that cause real bugs. Everything here reflects the specifications, browser source code and release notes as of September 2026.

Key takeaways

  • The core W3C specification defines only 13 top-level members plus the *_localized family and color_scheme_dark. Most integration members are Chromium incubations that Safari and Firefox ignore.
  • Invalid values never throw. The member, or the offending list entry, is dropped and its default applies; Chromium reports each case in the console with a Manifest: prefix and in DevTools.
  • start_url, id and scope depend on each other. Set all three explicitly, and set id before you ever change start_url.
  • URL-valued members resolve against the manifest URL (id is the exception: it resolves against the origin of start_url). Navigational URLs must be same-origin with the document and usually within scope.
  • name, short_name and icons, and their *_localized forms, are security-sensitive: after installation, browsers apply changes to them only with the user's approval.
  • Enhanced display modes (window-controls-overlay, tabbed) are only valid inside display_override. In display they are ignored and the default browser applies.

How to read this reference

Each member section starts with the same definition table:

Field Meaning
Type The JSON type the processing algorithm accepts. Any other type is ignored as if the member were absent.
Valid values The values that survive validation. Anything else is ignored.
Default The value after processing when the member is absent or invalid. "None" means the processed manifest has no value and the browser uses its own behavior.
Defined in The specification or vendor documentation that defines the member, with the section number for the core specification.
Status Standard (W3C Working Draft), W3C Note (Application Information Group Note), Incubation (WICG document, shipped in Chromium), Vendor (one company's product), or Proposal (not shipped by default anywhere).
Support First browser versions that act on the member, from MDN's browser-compat-data (September 2026) and vendor release notes. Where the two disagree, both are given.

Then come three blocks: Processing and validation (what the specification's algorithm and Chromium's parser actually do), a JSON example, and Gotchas.

Processing conventions that apply everywhere

These rules come from the core specification's processing algorithms and from Chromium's manifest parser (ManifestParser in Blink), which is the parser behind Chrome, Edge, Opera and Samsung Internet:

  • "Ignored" means absent. The specification says that when instructed to ignore, a browser "MUST act as if whatever manifest, member, or value caused the condition is absent".
  • No type coercion. A number where a string is expected, or the string "true" where a boolean is expected, is ignored. Nothing is ever converted between types.
  • Whitespace is trimmed. String values are stripped of leading and trailing ASCII whitespace before validation.
  • Enumerations are case-insensitive. The specification ASCII-lowercases display, orientation and dir before comparing, and Chromium compares its enumerations (display, orientation, client_mode, form_factor, purpose, launch_type) case-insensitively. Write lowercase anyway, because not every consumer is a browser.
  • URLs resolve against the manifest URL. A manifest at /static/app.webmanifest turns "start_url": "home" into /static/home. Always use root-relative paths. The details, including what happens with cross-origin and data: manifests, are in How browsers process a manifest.
  • Unknown members are ignored. That is what makes proprietary members and future members safe to ship.
  • Errors are reported, not thrown. Chromium logs each rejected value as a console warning prefixed with Manifest:, and the Application → Manifest pane of DevTools repeats them. The messages quoted on this page are Chromium's.

The core members are processed in a fixed order because later members depend on earlier ones:

flowchart LR
    DIR["dir"] --> LANG["lang"] --> NAME["name, short_name<br/>and *_localized"]
    NAME --> START["start_url<br/>(needs manifest URL<br/>and document URL)"]
    START --> ID["id<br/>(needs start_url origin)"]
    ID --> CHECK{"Same id as the<br/>document's current<br/>manifest?"}
    CHECK -->|"No"| STOP["Discard the new manifest"]
    CHECK -->|"Yes, or first manifest"| SCOPE["scope<br/>(start_url must be inside)"]
    SCOPE --> REST["colors, display, icons,<br/>color_scheme_dark, orientation"]
    REST --> SC["shortcuts<br/>(urls must be in scope)"]
    SC --> EXT["Extension point:<br/>display_override, share_target,<br/>file_handlers, launch_handler ..."]

Because the extension point runs last, every member defined outside the core specification sees the final scope, which is why integration members such as share_target.action and file_handlers[].action can be validated against it.

All members at a glance

Member Type Status Acted on by
name string Standard All engines with manifest support
short_name string Standard All engines with manifest support
description string W3C Note Chromium
id string (URL) Standard Chromium, Safari
lang string (language tag) Standard Limited (see section)
dir string Standard Limited (see section)
*_localized object (language map) Standard Chrome and Edge 148+ on desktop
migrate_from, migrate_to array, object Incubation Chrome 150+ on desktop
start_url string (URL) Standard All engines with manifest support
scope string (URL) Standard All engines with manifest support
scope_extensions array Incubation Chrome and Edge 139+ on desktop
launch_handler object Incubation Chromium 110+ on desktop
handle_links string Proposal None
display string Standard All engines with manifest support
display_override array Incubation Chromium 89+
orientation string Standard Android browsers
theme_color string (CSS color) Standard Chromium, Safari, Firefox for Android
background_color string (CSS color) Standard Chromium, Firefox for Android
color_scheme_dark object Standard None yet
tab_strip object Incubation ChromeOS (Chrome 126+)
icons array Standard All engines with manifest support
screenshots array W3C Note Chromium install dialog, stores
categories array of strings W3C Note Safari 17.4+ on macOS, stores
iarc_rating_id string W3C Note Stores only
related_applications array Incubation Chromium
prefer_related_applications boolean Incubation Chromium
shortcuts array Standard Chromium, Safari 17.4+ on macOS
share_target object Separate specification Chrome for Android, ChromeOS, Samsung Internet
file_handlers array Incubation Chromium 102+ on desktop
protocol_handlers array Incubation Chromium 96+ on desktop
note_taking object Incubation ChromeOS
edge_side_panel object Vendor (Microsoft) Microsoft Edge, being deprecated
widgets array Vendor (Microsoft) Edge on Windows 11
serviceworker object Vendor (Chromium) Payment handler installs
gcm_sender_id string Vendor (legacy) Obsolete
translations object Proposal Behind a Chromium flag

Identity and naming members

name

The full name of the application, shown wherever the operating system lists or labels apps, and the accessible name of the installed app.

Field Value
Type string
Valid values Any non-empty string
Default None (see the fallback rules below)
Defined in Web Application Manifest §1.4; a localizable member
Status Standard
Support Chrome and Chrome for Android 39, Edge 79, Safari 17 (macOS), Safari on iOS 11.3, Firefox for Android 79, Samsung Internet 4.0

Processing and validation

  • Processed with the specification's "process a text member" steps: the value must be a string and is trimmed; any other type is ignored (Chromium: property 'name' ignored, type string expected.).
  • Chromium also removes every carriage-return, line-feed and tab character anywhere in the string, and treats a string that is empty afterwards as missing.
  • When name is missing, empty or the wrong type, the specification's "application's name" rules let the browser use short_name instead, fall back to the document (for example <meta name="application-name">), assign a default such as "Untitled", or let the user type a name.
  • Installability: Chromium requires name or short_name and reports "Manifest does not contain a 'name' or 'short_name' field" otherwise. Firefox for Android's parser uses name, falls back to short_name, and rejects a manifest with neither.
  • Security-sensitive: the specification asks browsers to apply a changed name only with the user's "express permission". Since Chrome 144 on desktop, the change waits as an optional Review app update item in the app menu instead of a blocking dialog; Chrome on Android shows a confirmation dialog when the WebAPK is updated. See App identity & updates.
app.webmanifest
{
  "name": "Acme Tasks: Team To-Do Lists"
}

Gotchas

  • In standalone windows Chromium prepends short_name (or name when there is no short_name) to the document's <title>, so an app cannot disguise its window as a system dialog. Don't repeat the app name in <title> for installed windows. Since Chrome 134 on desktop, a <meta name="application-title" content="…"> tag replaces the <title> part of that title-bar text (chromestatus: "Document Subtitle (Fix PWA app titles)"); it is a Chromium-only, non-standard tag.
  • The specification asks browsers to display security-sensitive text bidirectionally isolated (Unicode UTS #55), so a name that mixes scripts cannot reorder surrounding browser UI.
  • Translate it with name_localized, not by serving a different manifest per user, wherever the browsers you target support it.

short_name

A shorter version of the name for places with little space: home-screen and launcher labels, taskbar tooltips, window titles.

Field Value
Type string
Valid values Any non-empty string
Default None (browsers may fall back to name)
Defined in Web Application Manifest §1.5; a localizable member
Status Standard
Support Same as name

Processing and validation

  • Same text processing as name, including Chromium's removal of CR, LF and tab characters.
  • When both are present, "it is left up to implementations to decide which member is best suited for the space available". web.dev's manifest guide states Chrome's rule: name is used when the app is installed, and short_name "on the user's home screen, launcher, or other places where space is limited".
  • Security-sensitive, like name.
app.webmanifest
{
  "name": "Acme Tasks: Team To-Do Lists",
  "short_name": "Tasks"
}

Gotchas

  • Launchers truncate long labels with an ellipsis. Aim for one word or a short brand name, and check the result on a real launcher.
  • Safari reads <meta name="apple-mobile-web-app-title">. If you keep that tag, give it the same value so users never see two different names.
  • Because Chromium uses short_name as the window-title prefix, a short_name like "Acme" produces title bars such as "Acme - Inbox". Check how it reads in the task switcher.

description

A human-readable description of what the app does. It is the accessible description of the installed app, and it appears in install dialogs and storefronts.

Field Value
Type string
Valid values Any string
Default None
Defined in Web App Manifest – Application Information (W3C Group Note, 21 August 2023) §2.2
Status W3C Note
Support Chrome, Edge and Chrome for Android 88; Samsung Internet 15.0. Firefox for Android parses it without effect; Safari ignores it.

Processing and validation

  • String, trimmed; other types are ignored. Unlike name, Chromium does not strip line breaks from it.
  • Chrome shows it under a Description heading in the richer install UI, together with screenshots, and truncates it at 300 characters (kMaximumDescriptionLength in Chromium).
  • Localizable in Chrome and Edge 148+ through description_localized (see *_localized).
app.webmanifest
{
  "description": "Plan, assign and track team tasks. Works offline and syncs when you reconnect."
}

Gotchas

  • A description alone does not switch Chrome to the richer install dialog; at least one screenshot for the current form factor is also required.
  • Write it as store copy: what the app does and for whom, in one or two sentences. It is not the page's meta description and not a keyword list.
  • description also exists inside shortcut items, where it describes the shortcut rather than the app.

id

A URL-shaped string that uniquely identifies the application. Browsers use it to decide whether a manifest describes an app that is already installed (an update) or a new app.

Field Value
Type string (parsed as a URL)
Valid values Any string that parses as a URL, relative or absolute, whose result is same-origin with start_url
Default The processed start_url, with its fragment removed
Defined in Web Application Manifest §1.11
Status Standard
Support Chrome, Edge and Chrome for Android 96; Safari 17 (macOS); Safari on iOS 16.4; Samsung Internet 17.0. Firefox parses it without effect.

Processing and validation

The specification's steps, in order:

  1. Set id to the processed start_url.
  2. If the value is not a string, or is the empty string, stop.
  3. Parse it with the origin of start_url as the base URL (not the manifest URL, and not start_url itself).
  4. If parsing fails, or the result is not same-origin with start_url, stop.
  5. Remove the fragment and use the result.

Chromium's ParseId implements exactly these steps. With a start_url of https://example.com/my-app/start, the specification's own examples resolve as follows:

id in the manifest Resulting identity
(absent) or "" https://example.com/my-app/start
"/" https://example.com/
"foo", "./foo", "/foo" https://example.com/foo
"foo?x=y" https://example.com/foo?x=y (the query is kept and is part of the identity)
"foo#heading" https://example.com/foo (fragment removed)
"https://anothersite.com/foo" https://example.com/my-app/start (cross-origin, ignored)
"😀" https://example.com/%F0%9F%98%80 (standard URL percent-encoding)

A manifest whose id matches an installed app is treated as a replacement for that app's manifest "even if it is served from a different URL". A manifest with an unknown id describes a distinct app, even if it is served from the same URL. The identity "doesn't point to a resource that can be navigated to", so it does not have to be within scope, and nothing is ever fetched from it.

There is also a runtime rule: if a document already has a processed manifest and a newly linked manifest has a different id, the specification stops processing and keeps the old one. Swapping the manifest link can rename an app, but cannot turn the page into a different app.

app.webmanifest
{
  "id": "/",
  "start_url": "/?source=pwa"
}

Gotchas

  • Treat id as permanent. Changing it strands every existing installation on the old identity, which then stops receiving manifest updates.
  • If your app shipped without id, its identity is its current start_url, query string included. Copy the Computed App Id from Chrome DevTools (Application → Manifest → Identity) into id verbatim: a start_url of /?source=pwa needs "id": "/?source=pwa" to keep existing installs.
  • Use a leading /. Because the base is the origin, "foo", "./foo", "../foo" and "/foo" all resolve identically, and the explicit form avoids confusion.
  • Identity comparison is an exact URL comparison: /app, /app/, /App/ and /app/?v=2 are four different apps.
  • WebKit added id in Safari 16.4 so that users can add the same site to the Home Screen several times (for example with work and personal accounts) with separate notifications and badges.
  • Deep dive: App identity & updates.

lang

The language of the manifest's default (non-localized) text values.

Field Value
Type string
Valid values A structurally valid BCP 47 language tag, such as en, en-AU or zh-Hans-CN
Default Unknown language
Defined in Web Application Manifest §1.3
Status Standard
Support MDN has no compatibility data for it. Chromium does not store a top-level lang, but since Chrome 148 reads it as the fallback language for *_localized entries that do not declare their own.

Processing and validation

  • Must be a string; it is trimmed and checked with ECMA-402's IsStructurallyValidLanguageTag. Invalid tags are ignored.
  • A valid tag is canonicalized with CanonicalizeUnicodeLocaleId, so "EN-us" becomes "en-US". Language tags are case-insensitive.
  • The specification explains its purpose: it helps browsers pick fonts, styling, hyphenation and text-to-speech voices for the app's name and other text.
app.webmanifest
{
  "lang": "de-DE",
  "name": "Acme Aufgaben"
}

Gotchas

  • lang does not select a translation; it labels the language of the default strings. Provide translations with *_localized.
  • If you negotiate the manifest per language on the server, set lang to match each variant, and keep id identical across all of them.

dir

The base text direction for the manifest's localizable text members (name, short_name, and the name, short_name and description of shortcut items).

Field Value
Type string
Valid values "ltr", "rtl", "auto"
Default "auto": direction is detected heuristically, for example with the first-strong algorithm of Unicode UAX #9
Defined in Web Application Manifest §1.2
Status Standard
Support MDN has no compatibility data. Chromium parses it (and logs unknown 'dir' value ignored. for bad values) and uses it for shortcut text.

Processing and validation

  • The specification first sets dir to "auto", then accepts a string that, after trimming and ASCII-lowercasing, is one of the three keywords.
  • It is processed first of all members, because every localizable member uses it as its default direction.
app.webmanifest
{
  "lang": "ar",
  "dir": "rtl",
  "name": "منتقي الألوان"
}

Gotchas

  • Set it explicitly for right-to-left languages instead of relying on detection, particularly when a name starts with a Latin brand name, which the first-strong heuristic would classify as left-to-right.
  • A localized entry whose direction differs from the manifest default must set its own dir (see the next section).

*_localized

The *_localized family provides translated values for localizable members. The browser picks the entry whose language tag best matches the user's language settings, and falls back to the plain member.

Field Value
Type object: a "language map" whose keys are language tags
Valid values For text members, each value is a string or a localized text object { "value", "lang"?, "dir"? }. For icons_localized, each value is an array of image resources.
Default None; the non-localized member (the "default representation") is used
Defined in Web Application Manifest §1.15 (added to the Working Draft in 2026); applies to name, short_name, icons and the name, short_name, description and icons of shortcut items
Status Standard
Support Chrome and Edge 148 on desktop (Chrome 148 release notes, "Manifest localization"). Not in Chrome for Android, Safari or Firefox.

Localizable members and their localized counterparts:

Member Localized member Value of each language-map entry
name name_localized string or { "value", "lang"?, "dir"? }
short_name short_name_localized string or object
description description_localized string or object. The top-level description lives in the Application Information Note, but Chromium and MDN treat it as localizable.
icons icons_localized array of image resources
shortcuts[].name, .short_name, .description, .icons the same *_localized names inside each shortcut item as above

Processing and validation

  • The member must be an object (Chromium: property 'icons_localized' ignored, type object expected.). Each key is a language tag; keys that are not valid tags are skipped (Chromium: property 'name_localized' entry for 'xx_YY' ignored, invalid locale key.).
  • A string entry is normalized to { "value": …, "lang": <key>, "dir": <manifest dir> }. An object entry needs a string value; lang and dir are optional overrides, and an invalid dir is ignored. An entry without a usable value is dropped.
  • Chromium differs from the specification in two details: an entry without its own lang gets the manifest-level lang (not the key), and an entry without its own dir gets auto (not the manifest's dir). Set both explicitly when they matter.
  • Selection: "the user agent SHOULD use the user's localization settings to select the localized value whose language tag key best matches the user's preference." MDN documents the matching as most specific first: for a browser language of fr-CA, Chrome tries fr-CA, then fr, then the default member.
  • icons_localized entries are complete replacements. When a locale matches, only its icons are used; they are not merged with icons.
  • Localized names and icons are security-sensitive. If the user changes language, the specification lets the browser switch the visible name and icon, and asks that the change be "presented to users the next time they open the web application".
app.webmanifest
{
  "lang": "en-US",
  "dir": "ltr",
  "name": "Color Picker",
  "name_localized": {
    "de": "Farbwähler",
    "en-GB": { "value": "Colour Picker", "lang": "en-GB" },
    "fr": { "value": "Sélecteur de Couleur", "lang": "fr-CA" },
    "ar": { "value": "منتقي الألوان", "lang": "ar", "dir": "rtl" }
  },
  "icons": [
    { "src": "/icons/icon-192.png", "sizes": "192x192", "type": "image/png" },
    { "src": "/icons/icon-512.png", "sizes": "512x512", "type": "image/png" }
  ],
  "icons_localized": {
    "ja": [
      { "src": "/icons/ja/icon-192.png", "sizes": "192x192", "type": "image/png" },
      { "src": "/icons/ja/icon-512.png", "sizes": "512x512", "type": "image/png" }
    ]
  },
  "shortcuts": [
    {
      "name": "New palette",
      "name_localized": { "de": "Neue Palette", "fr": "Nouvelle palette" },
      "url": "/palettes/new"
    }
  ]
}

Gotchas

  • There is no shortcuts_localized; localize each shortcut's own fields.
  • Provide every icon size in each icons_localized variant, or users of that locale lose the large icons (and a variant without a 144 px or larger any icon can break Chromium's installability check for them).
  • The entry-level lang exists for brand names that must be pronounced in another language than the user's locale; the specification's example is an English brand name ("Super Cookies") shown to German-speaking users with "lang": "en".
  • Browsers without support use the default members, so adopting *_localized is safe progressive enhancement. Server-side negotiation remains the only option for Safari and Chrome for Android; see Dynamic and generated manifests.

migrate_from and migrate_to

A same-site origin migration mechanism: a new app claims an installed app that lives on another origin of the same site (for example www.example.com to app.example.com), and the old origin confirms it, so users keep a single installation.

Field Value
Type migrate_from: array of strings or objects. migrate_to: object.
Valid values migrate_from entries: a string (the old app's id) or { "id", "install_url"?, "behavior"? } with behavior either "suggest" or "force". migrate_to: { "id", "install_url"? }.
Default None (behavior defaults to "suggest" in Chromium)
Defined in Manifest Incubations (WICG)
Status Incubation
Support Chrome 150 on desktop, per the Chrome 150 release notes ("PWA origin migration"); MDN's data lists Chrome and Edge 149. No other engine.

Processing and validation

  • migrate_from is only processed when the new manifest has an explicit, valid id. Otherwise Chromium logs property 'migrate_from' ignored, manifest must specify an 'id' property in order to receive a migration.
  • Unlike the top-level id, the ids inside migrate_from and migrate_to are resolved against the manifest URL. Chromium requires them to be same site with the document (migrate_from entry ignored, id should be same site as document.), and install_url, when present, must be same-origin with the entry's id.
  • install_url points to a page that links the other app's manifest, so the browser can fetch that manifest even when every other URL of the old app redirects to the new one.
  • behavior is a hint about how insistent the migration UI should be. Chromium takes the first recognized value and falls back to "suggest".
  • Validation of the migration itself: the old and new identities must be same site. If they are different origins, the old origin must serve /.well-known/web-app-origin-association with an entry keyed by the new app's id containing "allow_migration": true. Without that file, a hostile subdomain could claim someone else's installation.
  • migrate_to in the old manifest is an optional, proactive signal. Its target's manifest must list the old app in migrate_from for anything to happen.
  • Chrome does not offer migration for apps force-installed through the WebAppInstallForceList enterprise policy; it shows an explanatory banner instead.
https://app.example.com/app.webmanifest
{
  "id": "/",
  "name": "Example",
  "start_url": "/",
  "migrate_from": [
    {
      "id": "https://www.example.com/app/",
      "install_url": "https://www.example.com/app/install",
      "behavior": "suggest"
    }
  ]
}
https://www.example.com/app/app.webmanifest
{
  "id": "/app/",
  "name": "Example",
  "start_url": "/app/",
  "migrate_to": {
    "id": "https://app.example.com/",
    "install_url": "https://app.example.com/install"
  }
}
https://www.example.com/.well-known/web-app-origin-association
{
  "https://app.example.com/": {
    "allow_migration": true
  }
}

Gotchas

  • Cross-site moves (example.com to example.net) are refused by design.
  • Write the ids inside migrate_from and migrate_to as absolute URLs. A relative value resolves against the manifest URL, not against the origin as the top-level id does.
  • Keep the old origin serving its manifest (or at least the install_url page) until migrations are done; that is how existing installs discover the move.
  • Storage, permissions granted to the old origin and push subscriptions do not move with the app. The full procedure is in App identity & updates.

Launch and navigation members

start_url

The URL the app opens when the user launches it from its icon.

Field Value
Type string (parsed as a URL)
Valid values A URL, relative to the manifest URL or absolute, that is same-origin with the document that linked the manifest
Default The URL of the document that linked the manifest
Defined in Web Application Manifest §1.10
Status Standard
Support Chrome and Chrome for Android 39, Edge 79, Safari 17 (macOS), Safari on iOS 11.3, Firefox for Android 79, Samsung Internet 4.0

Processing and validation

  1. Set start_url to the document URL.
  2. If the value is missing, not a string, or empty, stop.
  3. Parse it with the manifest URL as the base; stop on failure (Chromium: property 'start_url' ignored, URL is invalid.).
  4. If the result is not same-origin with the document URL, stop (Chromium: property 'start_url' ignored, should be same origin as document.).
  5. Use the result.

For a manifest at https://example.com/resources/manifest.webmanifest, "../start_point.html" resolves to https://example.com/start_point.html (the specification's example).

The specification calls start_url "purely advisory": a browser may ignore it, or let the user edit it when installing (Safari's Add to Dock sheet does exactly that). Launching creates a new application context and navigates it to the start URL with history handling set to replace, so the back button never leads to a blank pre-launch entry.

app.webmanifest
{
  "start_url": "/?source=pwa"
}

The three identity members interact like this:

flowchart TD
    DOC["Document URL"] --> SU{"start_url valid and<br/>same-origin with document?"}
    MAN["Manifest URL (base)"] --> SU
    SU -- Yes --> S1["start_url = parsed value"]
    SU -- No --> S2["start_url = document URL"]
    S1 --> IDQ{"id valid and same-origin<br/>with start_url?"}
    S2 --> IDQ
    IDQ -- Yes --> I1["id = parsed id, no fragment"]
    IDQ -- No --> I2["id = start_url, no fragment"]
    S1 --> SCQ{"start_url within<br/>declared scope?"}
    S2 --> SCQ
    SCQ -- Yes --> C1["scope = declared scope,<br/>no query or fragment"]
    SCQ -- No --> C2["scope = start_url's directory"]

Gotchas

  • Chromium's installability check requires an explicitly specified, valid, same-origin start_url; the document-URL default does not count ("Manifest start URL is not valid"). The processed manifest records this as has_valid_specified_start_url.
  • Relative values resolve against the manifest URL. A manifest hosted on a CDN origin turns "/" into a CDN URL, which fails the same-origin check and is dropped.
  • Without an id, the identity is derived from start_url, so editing start_url creates a new app. Set id first.
  • start_url must be within scope, or the scope member is discarded.
  • Never encode user identity in it. The specification calls identifiers such as ?user=123 a fingerprint "that is not cleared when the user clears site data". A generic marker such as ?source=pwa for analytics is common practice.
  • The start URL is the first thing a user sees after a cold launch: make it work offline and for signed-out users. Offline strategies are in Offline UX & fallbacks.

scope

The navigation scope: the set of URLs that belong to the app. While the user stays within scope, the manifest remains applied; outside it, the browser shows its own UI.

Field Value
Type string (parsed as a URL)
Valid values A URL, relative to the manifest URL or absolute, such that the processed start_url is within it
Default start_url with its file name, query and fragment removed (/shop/cart.html?x=1 gives /shop/)
Defined in Web Application Manifest §1.6 and §5
Status Standard
Support Chrome and Chrome for Android 53, Edge 79, Safari 17 (macOS), Safari on iOS 11.3, Firefox for Android 79, Samsung Internet 6.0

Processing and validation

  1. Set scope to "." resolved against start_url (its directory).
  2. If the value is the empty string, stop. (Chromium also ignores a non-string with property 'scope' ignored, type string expected.)
  3. Parse it with the manifest URL as the base; stop on failure.
  4. Remove the query and fragment.
  5. If start_url is not within the parsed scope, stop (Chromium: property 'scope' ignored. Start url should be within scope of scope URL.).
  6. Use the result.

The within-scope test is deliberately simple: same origin, and the target's path string starts with the scope's path string. The specification calls it a prefix match "for consistency with Service Workers", not a path-segment match:

scope URL Within scope?
/app/ /app/, /app/settings/profile Yes
/app/ /app, / No
/app /app-legacy/index.html, /application Yes (prefix match)
/ any path on the origin Yes
/ the same path on another origin No

The query string of the target URL plays no part in the test, so /app/?tab=2 is within /app/.

Out-of-scope navigation is not blocked. Earlier drafts required it, which broke third-party sign-in, so the specification now asks browsers to keep the page in the app window and show "a prominent UI element indicating the URL or at least its origin", visibly different from the in-scope UI. web.dev's guidance adds that on Android, links with target="_blank" open in a Chrome Custom Tab. Safari on macOS Sequoia (Safari 18) uses scope in the other direction too: a link clicked in another application opens in the Dock web app whose scope it matches.

app.webmanifest
{
  "start_url": "/app/?source=pwa",
  "scope": "/app/"
}

Gotchas

  • End the scope with /, as the prefix table shows.
  • A start_url of /app is not within a scope of /app/, so that combination silently discards your scope and falls back to /.
  • The manifest scope and the service worker registration scope are independent. If the manifest scope is wider than the worker's, in-scope pages outside the worker's control have no offline support.
  • One scope covers one origin and one path prefix. Use scope_extensions for additional origins.
  • Always declare it; the specification recommends "/" for most apps. Without it, the scope depends on which page the user installed from.
  • Scope is presentation, not security: it decides which UI surrounds a page, not which URLs the app may load.

scope_extensions

Extends the app's navigation scope to other origins (subdomains, country domains, partner domains) that explicitly agree to the association.

Field Value
Type array of objects
Valid values { "type": "origin", "origin": "https://…" } entries whose origin is an https origin with a registrable domain
Default Empty list
Defined in Manifest Incubations (WICG)
Status Incubation
Support Chrome and Edge 139 on desktop, per the Chrome 139 release notes ("Web app scope extensions"); MDN's data lists 138, including Chrome for Android and Samsung Internet 30.0, where the member is parsed but not applied. No other engine.

Processing and validation

  • The value must be an array (property 'scope_extensions' ignored, type array expected.). Each entry must be an object with both type and origin (scope_extensions entry ignored, required properties 'type' and 'origin' are missing.); only type: "origin" is defined.
  • origin must parse as an https origin; any path is discarded. Chromium also rejects hosts that are a bare public suffix, limits the list to 10 entries and each origin string to 2,000 characters, and logs the reason for every rejected entry.
  • Each listed origin must confirm the association by serving /.well-known/web-app-origin-association: a JSON object whose keys are app ids (the full processed id URL) and whose values may contain a scope path, resolved against that origin and defaulting to "/".
  • A wildcard subdomain form (https://*.example.com) from the origin trial is parsed by Chromium only behind a feature flag that is disabled by default.
https://example.com/app.webmanifest
{
  "id": "https://example.com/app",
  "start_url": "/app/index.html",
  "scope": "/app/",
  "display": "standalone",
  "scope_extensions": [
    { "type": "origin", "origin": "https://example.co.uk" },
    { "type": "origin", "origin": "https://help.example.com" }
  ]
}
https://help.example.com/.well-known/web-app-origin-association
{
  "https://example.com/app": {
    "scope": "/"
  }
}

Gotchas

  • The key in the association file must equal the processed id exactly (scheme, host and path). A mismatch fails silently.
  • The association file is fetched by the browser, not by your page, so it must be publicly reachable (no login, no bot challenge) and return valid JSON.
  • Only navigation scope changes. Storage, cookies, permissions and service workers stay per origin, and handler URLs in share_target, file_handlers or protocol_handlers must still be within the manifest's own scope.
  • While the user is on an extended origin, the specification asks browsers to keep showing which origin they are on, distinct from the out-of-scope UI.
  • Syntax history, the older origin-trial shapes and link-capturing interactions: Advanced & integration members.

launch_handler

Controls what happens when the app is launched while it is already running: open a new window, navigate an existing one, or just focus it and let the page decide.

Field Value
Type object with a client_mode member, which is a string or an array of strings
Valid values client_mode: "auto", "navigate-new", "navigate-existing", "focus-existing"
Default { "client_mode": "auto" }
Defined in Web App Launch Handler API (WICG)
Status Incubation
Support Chrome and Edge 110 on desktop. MDN also lists Chrome for Android 110 and Samsung Internet 21.0, but window.launchQueue is not available in either, so the member has no script-visible effect there.

client_mode values

Value Behavior
auto The browser decides. Chrome's documentation: mobile devices support single clients and use the existing one; desktop uses navigate-new "to avoid data loss".
navigate-new A new browsing context in an app window loads the launch's target URL.
navigate-existing The most recently interacted-with app window is focused and navigated to the target URL.
focus-existing The most recently interacted-with app window is focused but not navigated; a LaunchParams object with the target URL is enqueued in its window.launchQueue.

If no app window exists, both -existing modes open a new window at the target URL.

Processing and validation

  • launch_handler must be an object (Chromium: launch_handler value ignored, object expected.).
  • client_mode may be an array; the first recognized value wins, which lets you list a future mode first with a fallback after it. Unrecognized entries are skipped with client_mode value '…' ignored, unknown value.; if none is valid, auto applies.
  • It applies to every launch: app icon, shortcuts, file handlers, protocol handlers, and links captured into the app. Link capturing covers only navigations that would open a new browsing context (a new tab or window, or a link opened from another app). It is on by default on Windows, macOS and Linux (since Chrome 138 for apps that declare launch_handler.client_mode, Chrome 140 for all apps, after a staged rollout from Chrome 134); on ChromeOS it is available but off by default per app. launch_handler is the developer control.
  • Since Chrome 146, LaunchParams.targetURL is populated for file-handling launches directed at an existing window (it used to be null), and a page reload no longer re-delivers the previous LaunchParams.
app.webmanifest
{
  "launch_handler": {
    "client_mode": ["focus-existing", "auto"]
  }
}
src/launch.js
// With focus-existing, the page must route the launch itself.
if ("launchQueue" in window) {
  window.launchQueue.setConsumer(async (launchParams) => {
    try {
      // File launches deliver FileSystemFileHandle objects.
      if (launchParams.files?.length) {
        await openFiles(launchParams.files); // your file-opening code
        return;
      }
      if (launchParams.targetURL) {
        const target = new URL(launchParams.targetURL);
        // Route in-app without reloading, preserving unsaved state.
        router.navigate(`${target.pathname}${target.search}`); // your router
      }
    } catch (error) {
      console.error("Failed to handle launch", error);
    }
  });
}

Gotchas

  • navigate-existing discards whatever the user had open in that window; prefer focus-existing plus a launchQueue consumer for editors, players and chat apps.
  • Set the consumer early during startup. Launch parameters are queued until a consumer exists, and a focus-existing launch with no consumer just focuses the window.
  • Chromium's parser reads only client_mode. Fields from the 2021–2022 origin trial, such as route_to, are ignored, so manifests copied from old articles silently get auto.
  • Full API details: Protocol handlers & launch handling and Advanced & integration members.

Experimental

handle_links is a proposal. Its chromestatus entry ("Web app handle links") is marked "On hold", and no browser ships it. The link capturing that did ship is controlled by launch_handler and by user settings.

A proposed opt-in or opt-out for having in-scope links open in the installed app instead of the browser.

Field Value
Type string
Valid values "auto", "preferred", "not-preferred"
Default "auto"
Defined in WICG explainer in the "PWAs as URL handlers" repository
Status Proposal
Support None
  • preferred: the browser should open in-scope links in the app, and may promote this to the user.
  • not-preferred: the browser should not open links in the app.
  • auto: a platform-specific choice.
app.webmanifest
{
  "handle_links": "preferred"
}

Gotcha: browsers ignore it today; do not rely on it to prevent link capturing. The related "Progressive Web Apps as URL Handlers" (url_handlers) and "Declarative Link Capturing" (capture_links) proposals are both marked "No longer pursuing" on chromestatus.

Presentation members

display

The developer's preferred display mode: how much browser UI surrounds the app when it runs as an installed app.

Field Value
Type string
Valid values "fullscreen", "standalone", "minimal-ui", "browser"
Default "browser"
Defined in Web Application Manifest §1.8 and §6
Status Standard
Support Chrome and Chrome for Android 39, Edge 79, Firefox for Android 47, Samsung Internet 4.0 (all four modes). Safari 17 (macOS) and Safari on iOS 11.3 support standalone and browser only.
Mode Specification definition (abridged) Fallback chain
fullscreen Browser UI hidden; the app takes up the entire available display area standalone → minimal-ui → browser
standalone Looks and feels like a standalone native app: own window, own launcher icon, no URL bar; system UI such as a status bar or back button may remain minimal-ui → browser
minimal-ui Like standalone, plus a minimal set of navigation controls (back, forward, reload, perhaps a way to view the address) browser
browser Opens with the platform's convention for opening links: a tab or a new window none

Processing and validation

  • Must be a string; it is trimmed and ASCII-lowercased, then must be one of the four modes. Anything else is ignored and browser applies (Chromium: unknown 'display' value ignored.).
  • Chromium explicitly rejects the enhanced modes here: "display": "window-controls-overlay" or "tabbed" logs inapplicable 'display' value ignored. Those belong in display_override.
  • To choose the mode, the browser first gives other specifications a chance (that is where display_override hooks in), then uses display if it supports it, then walks its fallback chain. Every browser must support browser, so the chain always ends somewhere.
  • The applied mode, not the requested one, is what the display-mode CSS media feature reports.
  • Chromium only treats a page as installable when the effective mode is standalone, fullscreen or minimal-ui, or an enhanced mode from display_override. Firefox for Android requires anything other than browser.
app.webmanifest
{
  "display": "standalone"
}

Gotchas

  • The fullscreen display mode is independent of the Fullscreen API: an app can be in fullscreen mode while document.fullscreenElement is null.
  • On iOS and iPadOS, MDN's compatibility notes record that a Home Screen web app with "display": "standalone" matches display-mode: fullscreen rather than standalone (WebKit bug 264218); a web app added without a manifest reports browser, and a manifest "display": "fullscreen" opens as standalone. Check navigator.standalone === true first, then the media query; see Detecting installed apps.
  • Since iOS 26 and iPadOS 26, every site added to the Home Screen opens as a web app by default, whatever display says; users can turn "Open as Web App" off in the Add to Home Screen sheet. On macOS, every site added to the Dock has opened as a web app since Safari 17.
  • MDN's data lists no support for the display-mode: fullscreen media feature in Chrome for Android, so do not rely on that query there; test the mode you ask for on real devices.
  • Full behavior and CSS patterns: Display modes.

display_override

An ordered list of display modes that the browser tries before display. It lets you choose your own fallback order and use modes that display cannot express.

Field Value
Type array of strings (object entries are reserved for URL-dependent modes)
Valid values "fullscreen", "standalone", "minimal-ui", "browser", "window-controls-overlay", "tabbed"; "unframed" for Isolated Web Apps only
Default Empty list
Defined in Manifest Incubations (WICG); window-controls-overlay in the Window Controls Overlay draft
Status Incubation
Support Chrome, Edge and Chrome for Android 89; Samsung Internet 15.0. window-controls-overlay: Chrome and Edge 105 on desktop. tabbed: Chrome 126, shipped on ChromeOS only. unframed: Isolated Web Apps only; developer trial from Chrome 146, shipped in Chrome 152 on ChromeOS only (gradual rollout).
Entry Meaning Where it works
fullscreen, standalone, minimal-ui, browser The basic modes, in your chosen order Chromium
window-controls-overlay The app draws into the title-bar area and the window controls float above the content Chromium desktop (Window Controls Overlay)
tabbed Several app contexts share one window with a tab strip; configure it with tab_strip ChromeOS; behind a flag elsewhere
unframed No title bar or window controls at all Isolated Web Apps only

Processing and validation

  • Must be an array (Chromium: property 'display_override' ignored, type array expected.). Each string entry is trimmed and compared case-insensitively; unknown or unsupported values are dropped silently, without a console message.
  • Chromium drops tabbed entries unless its tab-strip feature is enabled for the platform, and drops unframed unless the Isolated Web App feature is enabled, so on most desktops those entries simply fall through.
  • The incubation also allows object entries of the form { "display": …, "url_patterns": [...] } for URL-dependent display modes. Chromium accepts url_patterns only for unframed and otherwise logs display override '…' ignored, url_patterns are not allowed.
  • The first supported entry wins. If none is supported, display is evaluated with its normal fallback chain. web.dev's guidance adds that when there is no display member, the browser ignores display_override altogether, so always keep display.
  • For installability, Chromium evaluates the first recognized entry. If that entry is not an installable mode (for example browser), it reports "Manifest contains 'display_override' field, and the first supported display mode must be one of 'standalone', 'fullscreen', or 'minimal-ui'".
app.webmanifest
{
  "display_override": ["window-controls-overlay", "minimal-ui"],
  "display": "standalone"
}

On Chromium desktop this app gets the overlay title bar; Chromium on Android does not support the overlay and uses minimal-ui; Safari and Firefox ignore display_override and use standalone.

Gotchas

  • Typos are invisible. "window-control-overlay" (singular, as printed in web.dev's own display_override example) is silently skipped. Check the Presentation section in DevTools.
  • Do not put browser first casually: as the first recognized entry it makes the app non-installable in Chromium.
  • window-controls-overlay gives you the title-bar area but also the job of drawing a draggable region; without that CSS the window cannot be moved. See Window Controls Overlay.

orientation

The default screen orientation for all of the app's top-level browsing contexts.

Field Value
Type string
Valid values "any", "natural", "landscape", "portrait", "portrait-primary", "portrait-secondary", "landscape-primary", "landscape-secondary" (the Screen Orientation API's OrientationLockType values)
Default None; the device's own orientation behavior applies
Defined in Web Application Manifest §1.9
Status Standard
Support Chrome for Android 39, Firefox for Android 79, Samsung Internet 4.0. MDN also lists desktop Chrome 39, where app windows do not rotate. Not supported by Safari.

Processing and validation

  • Trimmed and ASCII-lowercased; must be one of the eight values or it is ignored (Chromium: unknown 'orientation' value ignored.).
  • If the browser supports the value, it becomes the default orientation "for the life of the web application", and the browser "MUST return the orientation to the default screen orientation any time the orientation is unlocked" or the top-level context navigates.
  • Browsers may refuse combinations that make no sense. The specification's example is an orientation lock in browser display mode.
  • At runtime, screen.orientation.lock() can change it temporarily where the browser allows locking, and screen.orientation.unlock() returns to the manifest default.
Value Effect on a typical phone
any Rotates freely with the device
natural The device's natural orientation (portrait for most phones, landscape for many tablets)
portrait / landscape Either variant of that orientation, following rotation
portrait-primary / landscape-primary Fixed to the primary variant
portrait-secondary / landscape-secondary Fixed to the upside-down variant
app.webmanifest
{
  "orientation": "portrait-primary"
}

Gotchas

  • Locking a phone app to portrait also locks it on tablets and foldables, where it can end up letterboxed. Prefer any plus a responsive layout unless the content genuinely requires one orientation (a game, a camera tool).
  • On Android the orientation is baked into the WebAPK, so a change arrives with the next WebAPK update rather than immediately (orientation is one of the fields Chrome compares when deciding to update; see App identity & updates).

theme_color

The default theme color of the app: Android's status bar and task switcher, the title bar of an installed desktop app, and similar browser-controlled surfaces.

Field Value
Type string
Valid values Any CSS color that can be converted to sRGB without outside information: hex, named colors, rgb(), hsl(), lab(), color(display-p3 …) and so on
Default None; the browser's own color applies
Defined in Web Application Manifest §1.12 (a "themeable member")
Status Standard
Support Chrome and Chrome for Android 46, Edge 79, Safari 17 (macOS), Safari on iOS 15, Firefox for Android 79, Samsung Internet 5.0

Processing and validation

  • Processed with the specification's "process a color member" steps: must be a string, is trimmed, then parsed as a CSS color; unparsable values are ignored (Chromium: property 'theme_color' ignored, '#12345' is not a valid color.).
  • Colors are converted to sRGB. lab() or color(display-p3 …) can be converted "without outside knowledge"; color(--custom-profile …) cannot, because it needs an @color-profile rule the manifest cannot carry, so it is rejected.
  • The browser "MAY ignore the theme color's alpha component"; in most environments a theme color cannot be transparent.
  • A <meta name="theme-color"> in an in-scope document may override the manifest color; browsers "SHOULD NOT" let out-of-scope documents do so. Browsers may also override it to support prefers-color-scheme.
app.webmanifest
{
  "theme_color": "#0b57d0"
}

Gotchas

  • The manifest has no working dark variant yet (color_scheme_dark is unimplemented). Use two <meta name="theme-color"> tags with media="(prefers-color-scheme: …)"; Chrome honors the media attribute since Chrome 93.
  • Since Safari 26 (macOS, iOS and iPadOS), MDN's compatibility notes record that <meta name="theme-color"> is only used for installed web apps, no longer for regular Safari tabs. Chrome and Edge on desktop likewise use the meta color only in installed apps, so on desktop the theme color is effectively an installed-app feature everywhere.
  • Pick a color with enough contrast for the system's status-bar and title-bar text. Details on contrast and status bars: Splash screens & theming.

background_color

The expected background color of the app's pages, used before the stylesheet is available, most visibly on the Android launch splash screen.

Field Value
Type string
Valid values Same as theme_color
Default None
Defined in Web Application Manifest §1.13 (a "themeable member")
Status Standard
Support Chrome and Chrome for Android 46, Edge 79, Firefox for Android 79, Samsung Internet 5.0. Not supported by Safari.

Processing and validation

  • Same color processing as theme_color.
  • The specification restricts its use: it is "only meant to improve the user experience while a web application is loading and MUST NOT be used by the user agent as the background color when the web application's stylesheet is available".
  • Chrome builds the Android splash screen from name, background_color and the icon that best fits the screen density. Firefox for Android, when both colors are set, colors its navigation bar black or white depending on whether background_color is dark.
app.webmanifest
{
  "background_color": "#ffffff"
}

Gotchas

  • Match your CSS body background exactly, or users see a color flash between the splash screen and first paint.
  • iOS and iPadOS do not generate a splash screen from the manifest; they use <link rel="apple-touch-startup-image"> images if you provide them. See Splash screens & theming.
  • If your app has a dark theme, a light background_color produces a bright flash on every launch on Android. Until color_scheme_dark ships, pick the color your most common theme paints first.

color_scheme_dark

Experimental

color_scheme_dark is in the W3C Working Draft, but no browser implements it as of September 2026: Chromium's manifest parser does not read it, and MDN has no compatibility entry for it. Chromium ran an origin trial of an earlier design ("Dark mode support for web apps", the user_preferences member) from Chrome 99, extended through Chrome 114 after the format was reworked around CSS media queries, and did not ship it.

Dark-scheme overrides for the two themeable members, applied when the operating system uses a dark theme "unless the user's preferences, such as accessibility settings, take precedence".

Field Value
Type object
Valid values Optional theme_color and background_color members, each a CSS color string
Default None
Defined in Web Application Manifest §1.16
Status Standard (unimplemented)
Support None

Each value is processed with the same color rules as the top-level members; a color_scheme_dark that is not an object is ignored, and so is any key other than the two themeable members.

app.webmanifest
{
  "theme_color": "#0b57d0",
  "background_color": "#ffffff",
  "color_scheme_dark": {
    "theme_color": "#0f172a",
    "background_color": "#020617"
  }
}

Gotcha: adding it today is harmless (unknown members are ignored) and future-proof, but keep the <meta name="theme-color" media> pair until browsers ship it.

tab_strip

Customizes the tab strip of the tabbed display mode: an optional pinned home tab and the URL opened by the new tab button.

Field Value
Type object
Valid values Optional home_tab object (with scope_patterns, and in Chromium icons) and optional new_tab_button object (with url)
Default { "new_tab_button": { "url": <start_url> } } and no home tab
Defined in Manifest Incubations (WICG)
Status Incubation
Support Shipped with tabbed mode on ChromeOS (Chrome 126). Chromium parses tab_strip only when its tab-strip customization feature is enabled; on other desktops both tabbed and tab_strip are behind flags.

Members

Member Type Meaning
home_tab.scope_patterns array of URL pattern inputs (strings or URLPatternInit objects), resolved against the manifest URL URLs that belong to the pinned home tab. The start URL is always part of it.
home_tab.icons array of image resources Chromium-specific icon for the home tab
new_tab_button.url string, within scope The URL a new tab opens. Default: the start URL.

Processing and validation

  • Takes effect only when display_override selects tabbed.
  • If the app has a home tab, every app window gets exactly one. Navigations from the home tab to URLs outside its scope open in a new tab; navigations in other tabs to URLs inside it are redirected to the home tab, which gets focus.
  • A URL is within the home tab scope if it is within the manifest scope and either equals the start URL (fragment ignored, but the query must match exactly) or matches one of the scope_patterns.
  • The new tab button is hidden when its URL is inside the home tab scope. With the default URL (the start URL, which is always in the home tab scope), an app that defines a home tab has no new tab button unless it sets new_tab_button.url.
  • history.pushState() changes and fragment changes do not move a document between the home tab and regular tabs; only real navigations do.
app.webmanifest
{
  "display": "standalone",
  "display_override": ["tabbed"],
  "tab_strip": {
    "home_tab": {
      "scope_patterns": [{ "pathname": "/" }, { "pathname": "/index.html" }]
    },
    "new_tab_button": {
      "url": "/documents/new"
    }
  }
}

Gotcha: single-page apps that fake navigation with the History API break the home-tab rules. Detect @media (display-mode: tabbed) and perform real navigations across the home-tab boundary. More in Advanced & integration members.

Icons and imagery

icons

The images that represent the app: launcher and home-screen icons, task switcher, window title bar, splash screen and install dialogs.

Field Value
Type array of image resource objects
Valid values Objects with a valid src, plus optional sizes, type, purpose and label
Default Empty list
Defined in Web Application Manifest §1.7 and §2 (image resources from the Image Resource specification, plus purpose); a localizable member
Status Standard
Support Chrome and Chrome for Android 39, Edge 79, Firefox for Android 79, Samsung Internet 4.0. Safari 17 (macOS) and Safari on iOS 15.4 use manifest icons only when no apple-touch-icon is present and the icon's purpose is any or absent.

Image resource members

Member Required Type Notes
src Yes string (URL) Resolved against the manifest URL. Chromium accepts http:, https:, data: and the document's own scheme, and drops others with property 'src' of 'icon' ignored, invalid scheme.
sizes No string Space-separated WxH tokens ("48x48 96x96") or "any" for scalable images, with the same grammar as <link rel="icon" sizes>
type No string (MIME type) A hint that lets the browser skip formats it cannot decode without downloading them
purpose No string Space-separated tokens from any, maskable, monochrome. Default any.
label No string Accessible name of the image, from the Image Resource specification

The purpose tokens

Token Meaning
any Usable in any context that does not require a special purpose (the default)
maskable Designed for masking: all important content lies inside the safe zone, a centered circle whose radius is 40% of the icon's smaller dimension. The browser may make anything outside it transparent, must not touch anything inside it, and must composite transparent pixels onto a solid fill.
monochrome Only the alpha channel is used; the browser paints the shape with a color of its choice (badges, pinned icons, themed launchers)

Processing and validation

  • If icons is not an array, the whole member is ignored (property 'icons' ignored, type array expected.). Entries that are not objects or have no parsable src are dropped.
  • purpose is split on whitespace and unknown tokens are discarded. If no valid token remains (for example "purpose": "fizzbuzz"), the icon is ignored entirely rather than treated as any, so that a future purpose can never be misused by old browsers. The specification's comparison is case-sensitive; Chromium's is not.
  • Chromium logs found icon with no valid size. when sizes contains no parsable token; such icons are kept but are rarely chosen, because selection works on declared sizes.
  • Icon fetches use request destination image, are governed by the linking document's CSP img-src directive, and pass through the service worker. When a manifest is processed without a document (for example during a background update), the specification forbids fetching icons at all.
  • Installability: Chromium needs an icon with purpose any, declared sizes of at least 144 px (or any), in PNG, SVG or WebP. Chrome's documentation asks for 192 × 192 and 512 × 512 icons. Firefox for Android needs an icon of at least 192 px with purpose any or maskable.
  • Security-sensitive. The specification says a browser "SHOULD consider a manifest image resource updated if the src member has changed", comparing icon URLs like Cache-Control: immutable. Since Chrome 144, desktop Chrome only re-downloads icons when the icons entries change, and applies changes of less than 10% (pixel comparison) without asking.
app.webmanifest
{
  "icons": [
    { "src": "/icons/icon-192.png", "sizes": "192x192", "type": "image/png" },
    { "src": "/icons/icon-512.png", "sizes": "512x512", "type": "image/png" },
    { "src": "/icons/maskable-192.png", "sizes": "192x192", "type": "image/png", "purpose": "maskable" },
    { "src": "/icons/maskable-512.png", "sizes": "512x512", "type": "image/png", "purpose": "maskable" },
    { "src": "/icons/monochrome.svg", "sizes": "any", "type": "image/svg+xml", "purpose": "monochrome" }
  ]
}

Gotchas

  • "purpose": "any maskable" on a single image is valid but usually looks wrong: the padding a maskable icon needs makes it look shrunken where it is used unmasked. Ship separate images.
  • A manifest with only maskable icons fails Chromium's installability check, which looks specifically for purpose any.
  • To change an icon for installed users, change its URL (for example put a content hash in the file name). Replacing the file at the same URL is not detected by Chrome 144 and later, and WebKit documents no mechanism that updates a Home Screen icon after it was added.
  • Chromium supports SVG icons, but web.dev notes they "stay in the state they were in at install time, instead of updating live", so they do not follow prefers-color-scheme afterwards. Safari 26 accepts SVG icons (and data: URLs for icons) too. Keep PNG fallbacks.
  • Declared sizes must match the real pixel dimensions: browsers pick icons by the declared size and then scale the actual pixels, so a 96 px file declared as 512x512 becomes a blurry splash icon.
  • Full guidance, including selection algorithms, platform sizes and generator scripts: Icons & maskable icons.

screenshots

Images of the app in use, shown by install dialogs and storefronts.

Field Value
Type array of screenshot objects
Valid values Image resources (src, sizes, type) plus optional label, form_factor ("narrow" or "wide") and platform
Default Empty list
Defined in Web App Manifest – Application Information (W3C Group Note) §2.4 and §3
Status W3C Note
Support Chrome's richer install UI: Chrome 94 on Android, Chrome 108 on desktop. Stores and packaging tools read it. Safari and Firefox ignore it.

Screenshot members

Member Values Meaning
src, sizes, type As for icons The image
label string Accessible name and alternative text. Provide one for every screenshot.
form_factor "narrow" or "wide" The screen shape the screenshot applies to; omit it if the screenshot applies to all
platform Operating systems: android, chromeos, ios, ipados, kaios, macos, windows, xbox. Distribution platforms: chrome_web_store, play, itunes, microsoft-inbox, microsoft-store. Only for screenshots that show platform-specific features. Chromium does not parse it.

Processing and validation

  • Non-array values are ignored (property 'screenshots' ignored, type array expected.); entries without a valid src are dropped.
  • Chromium matches form_factor case-insensitively. An invalid value logs property 'form_factor' on screenshots has an invalid value, ignoring it. and the screenshot is kept as form-factor-neutral.
  • The Note tells user agents not to show screenshots that do not pertain to their platform or form factor: phones shouldn't show wide screenshots.

Chromium's rules for the richer install UI, summarized from Rich install UI:

Rule Android (Chrome 94+) Desktop (Chrome 108+)
Screenshots used form_factor absent or narrow form_factor: "wide" only
Minimum to get the rich UI 1 1
Maximum used 8 8
Dimensions 320 to 3,840 px per side 320 to 3,840 px per side
Aspect ratio Long side at most 2.3 × the short side; all screenshots the same ratio Rendered width capped at 2.3 × the height
app.webmanifest
{
  "screenshots": [
    {
      "src": "/shots/board-wide.png",
      "sizes": "1920x1080",
      "type": "image/png",
      "form_factor": "wide",
      "label": "Task board with To do, Doing and Done columns"
    },
    {
      "src": "/shots/today-narrow.png",
      "sizes": "1080x2340",
      "type": "image/png",
      "form_factor": "narrow",
      "label": "Today's tasks on a phone"
    }
  ]
}

Gotchas

  • Screenshots that violate the size or ratio rules are dropped without any error on the page. Check the Screenshots section of the DevTools Manifest pane and test the install dialog itself.
  • Use real UI, not marketing composites; the install dialog is a trust surface.
  • Details, capture scripts and design guidance: Rich install UI.

categories

Hints for catalogs and stores about which categories the app belongs to.

Field Value
Type array of strings
Valid values Any strings; the Note lists known categories and encourages lowercase
Default Empty list
Defined in Web App Manifest – Application Information (W3C Group Note) §2.1
Status W3C Note
Support Safari 17.4 on macOS Sonoma and later. Chromium's manifest parser does not read it. Used by stores and packaging tools.

Processing and validation

  • Purely advisory: "catalogs and stores are not required to honor this hint", "like search engines and meta keywords".
  • The Note's list of known categories: beauty, books, books & reference, business, cars, dating, design, developer, developer tools, development, education, entertainment, events, fashion, finance, fitness, food, fundraising, games, government, graphics, graphics & design, health, health & fitness, kids, lifestyle, magazines, medical, multimedia, multimedia design, music, navigation, network, networking, news, parenting, personalization, pets, photo, photo & video, politics, productivity, reference, security, shopping, social, social networking, sports, transportation, travel, utilities, video, weather.
  • WebKit's Safari 17.4 announcement explains the one browser use: on macOS, when a user creates a Launchpad folder containing web apps, the folder is automatically named after their category.
app.webmanifest
{
  "categories": ["productivity", "business"]
}

Gotcha: stores have their own taxonomies; expect to set categories again in each store listing (see Publishing to app stores).

iarc_rating_id

The International Age Rating Coalition (IARC) certification code for the app, so storefronts can show an age rating.

Field Value
Type string
Valid values A single IARC certification code
Default None
Defined in Web App Manifest – Application Information (W3C Group Note) §2.3
Status W3C Note
Support No browser reads it; Chromium's parser ignores it and MDN no longer has a reference page for it. Intended for storefronts and packaging tools.
  • An IARC certificate is obtained through participating storefronts. The member takes a single code, and the Note says the same code can be shared across participating stores "as long as the distributed product remains the same (i.e., doesn't serve totally different code paths depending on user agent sniffing and the like)".
app.webmanifest
{
  "iarc_rating_id": "e84b072d-71b3-4d3e-86ae-31a8ce4e53b7"
}

Gotcha: the value above is the Note's own example; an invented code is worse than none. Omit the member unless you hold a real certificate.

A list of platform-specific applications (a native app, a store listing, or the web app itself) that are related to the web app. The relationship is one-way: the browser "MUST NOT assume a bi-directional endorsement" unless the other app claims the same relationship.

Field Value
Type array of external application resource objects
Valid values Objects with a non-empty platform and at least one of url or id
Default Empty list
Defined in Manifest Incubations (WICG)
Status Incubation
Support Chrome for Android 44 and Samsung Internet 4.0 (native-app promotion and navigator.getInstalledRelatedApps()); MDN lists Edge 17 and later. Desktop Chromium uses it for install-promotion decisions and getInstalledRelatedApps(). Firefox for Android parses it without effect.

Entry members

Member Required Meaning
platform Yes The distribution platform. The specification defines no values and points to a registry maintained by the Working Group; Chromium acts on play, windows, webapp and chrome_web_store.
url One of url or id Where the app can be found; resolved against the manifest URL with no origin restriction
id One of url or id The app's identifier on that platform: an Android package name for play, a package family name plus !App for windows, the manifest id for webapp
min_version No Minimum related version, platform-specific syntax. Chromium does not parse it.
fingerprints No Array of { "type", "value" } used to verify the app, for example sha256_cert for Android signing certificates. Chromium does not parse it.

Processing and validation

  • Non-arrays are ignored (property 'related_applications' ignored, type array expected.). Chromium drops entries without platform ('platform' is a required field, related application ignored.) and entries with neither a valid url nor an id (one of 'url' or 'id' is required, related application ignored.).
  • navigator.getInstalledRelatedApps() reads this list and resolves with the entries that are installed and verified from the other side: Chrome for Android 80 for Android apps and 84 for PWAs, Chrome and Edge 85 on Windows for packaged Windows apps, and (per the Chrome 140 release notes) installed web apps on desktop. An Android app proves the relationship through Digital Asset Links; a Windows app through its windows.appUriHandler declaration.
  • Chromium suppresses its own install promotion when a related non-web app from this list is already installed (for example the play package on Android), whatever prefer_related_applications says.
app.webmanifest
{
  "related_applications": [
    {
      "platform": "play",
      "url": "https://play.google.com/store/apps/details?id=com.example.tasks",
      "id": "com.example.tasks"
    },
    {
      "platform": "webapp",
      "url": "https://app.example.com/app.webmanifest",
      "id": "https://app.example.com/"
    }
  ]
}

Gotcha: listing an app does nothing visible on its own in most browsers. It becomes useful with getInstalledRelatedApps() (see Detecting installed apps) or with prefer_related_applications. Platform strings such as itunes or f-droid in older examples are not acted on by any browser.

Tells the browser to promote a related native app instead of the web app.

Field Value
Type boolean
Valid values true, false
Default false
Defined in Manifest Incubations (WICG)
Status Incubation
Support Chrome for Android 44 (the Play lookup works on the Beta and Stable channels only), Samsung Internet 4.0; MDN lists Edge 17 and later. Firefox for Android parses it without effect.

Processing and validation

  • Must be a JSON boolean. "true" (a string) is ignored (property 'prefer_related_applications' ignored, type boolean expected.) and the default false applies.
  • When true and the list contains an app on a platform the current device supports, Chromium stops its install promotion with the status "Manifest specifies prefer_related_applications: true". The web app remains installable from the browser menu; it just isn't promoted, and beforeinstallprompt doesn't fire for it.
  • On Android with a play entry, Chrome instead queries Google Play and offers the native app: beforeinstallprompt fires with platforms set to ["play"]. If the entry's url contains an id= parameter it must equal id, or Chrome rejects the entry. On Canary and Dev channels Chrome reports "prefer_related_applications is only supported on Chrome Beta and Stable channels on Android".
app.webmanifest
{
  "prefer_related_applications": true,
  "related_applications": [
    { "platform": "play", "id": "com.example.tasks" }
  ]
}

Gotcha: leave it false unless you really want Android users sent to the Play Store. MDN's compatibility notes go further and describe false as a requirement for installability in Chrome; either way, true costs you the PWA install promotion. The PWA-first alternatives are in Install prompts & custom UI.

OS integration members

shortcuts

Links to key tasks, shown in the app icon's context menu: long-press on Android, right-click or the jump list on Windows, the Dock menu and File menu on macOS.

Field Value
Type array of shortcut item objects
Valid values Objects with a non-empty name and a string url within scope, plus optional short_name, description, icons and their *_localized forms
Default Empty list
Defined in Web Application Manifest §1.14 and §3
Status Standard
Support Chrome for Android 84; Chrome and Edge 96 on desktop (85–95 on Windows only); Safari 17.4 on macOS Sonoma (File menu and Dock menu); Samsung Internet 14.0. Not in Safari on iOS or in Firefox.

Shortcut item members

Member Required Type Notes
name Yes string Label in the menu; must be non-empty
url Yes string (URL) Resolved against the manifest URL; must be within scope
short_name No string Used where space is limited
description No string "User agents MAY expose this information to assistive technology"
icons No array of image resources Same format as the top-level icons
name_localized, short_name_localized, description_localized, icons_localized No language maps See *_localized

Processing and validation

  • Non-arrays are ignored (property 'shortcuts' ignored, type array expected.). An item is dropped if it is not an object, has no non-empty name (property 'name' of 'shortcut' not present.), has no string url, or its url is not within scope (property 'url' ignored, should be within scope of the manifest.).
  • Chromium looks at no more than the first 10 entries of the array, counting invalid ones, and then logs property 'shortcuts' contains more than 10 valid elements, only the first 10 are parsed. Platforms show fewer; the specification lets browsers "truncate the list of shortcuts presented" to match OS conventions and asks them to keep the manifest order.
  • Invoking a shortcut runs the "launch a web application" steps with the shortcut URL, so launch_handler applies.
  • Like start_url, shortcut URLs must not carry user identifiers; the specification's privacy section calls that fingerprinting.
app.webmanifest
{
  "shortcuts": [
    {
      "name": "New task",
      "short_name": "New",
      "description": "Create a task in your inbox",
      "url": "/tasks/new?source=shortcut",
      "icons": [{ "src": "/icons/shortcut-new-96.png", "sizes": "96x96", "type": "image/png" }]
    },
    {
      "name": "Today",
      "url": "/tasks/today?source=shortcut",
      "icons": [{ "src": "/icons/shortcut-today-96.png", "sizes": "96x96", "type": "image/png" }]
    }
  ]
}

Gotchas

  • Put the most important shortcut first; platforms cut from the end.
  • Shortcuts are static. Changes reach installed apps through the manifest update process (immediately on Chrome 144+ desktop, with the next WebAPK update on Android), and there is no API to add shortcuts at runtime.
  • Per-platform limits, icon sizes, offline handling of shortcut URLs and tests: App shortcuts.

share_target

Registers the installed app as a target in the operating system's share sheet, so users can share text, links and files into it.

Field Value
Type object
Valid values action (URL within scope), method ("GET" or "POST"), enctype ("application/x-www-form-urlencoded" or "multipart/form-data"), params (object)
Default None
Defined in Web Share Target API (W3C editor's draft); file sharing from the "Web Share Target Level 2" work that Chromium implements
Status Separate specification (implemented only in Chromium)
Support Chrome for Android 76 (Level 2, including files), Chrome 89 on ChromeOS, Samsung Internet 12.0. Firefox for Android parses it without effect. Not in Safari.

Members

Member Required Values
action Yes URL within scope, on a potentially trustworthy origin; the share navigates to it
method No "GET" (default) or "POST", case-insensitive
enctype No "application/x-www-form-urlencoded" (default) or "multipart/form-data", case-insensitive; only meaningful with POST
params.title, params.text, params.url No The names of the query or form fields that receive each part of the shared data
params.files No An array (or single object) of { "name": <form field>, "accept": <MIME type or extension, or an array of them> }

Processing and validation

The specification and Chromium differ in strictness; write manifests that satisfy the stricter of the two:

Situation Specification Chromium
action or params missing Member ignored Member ignored
action outside scope Member ignored Member ignored (property 'share_target' ignored. Property 'action' is invalid.)
method missing GET GET, with a console warning
method other than GET or POST Member ignored Member ignored (invalid method. Allowed methods are:GET and POST.)
POST without enctype Member ignored application/x-www-form-urlencoded, with a warning
GET with multipart/form-data enctype ignored; urlencoded query parameters are used Member ignored (invalid enctype for GET method. Only application/x-www-form-urlencoded is allowed.)
params.title (or text, url) not a string Member ignored That parameter is ignored
files without POST + multipart/form-data Not covered by the specification text Member ignored (files are only supported with multipart/form-data POST.)
An invalid MIME type in any accept Not covered Whole member ignored (invalid mime type inside files.)

With GET, the shared data arrives as query parameters encoded as application/x-www-form-urlencoded, so spaces become +, which decodeURIComponent() does not decode (URLSearchParams does). Absent fields are omitted.

app.webmanifest
{
  "share_target": {
    "action": "/share-target",
    "method": "POST",
    "enctype": "multipart/form-data",
    "params": {
      "title": "title",
      "text": "text",
      "url": "url",
      "files": [
        { "name": "media", "accept": ["image/png", "image/jpeg", ".png", ".jpg", ".jpeg"] }
      ]
    }
  }
}
sw.js (share target handler)
self.addEventListener("fetch", (event) => {
  const url = new URL(event.request.url);
  if (event.request.method === "POST" && url.pathname === "/share-target") {
    event.respondWith(handleShare(event.request));
  }
});

async function handleShare(request) {
  const target = new URL("/compose", self.location.origin);
  try {
    const form = await request.formData();
    const cache = await caches.open("shared-files");

    // Stash each file so the page can pick it up after the redirect.
    for (const file of form.getAll("media")) {
      if (!(file instanceof File)) continue;
      const id = crypto.randomUUID();
      await cache.put(
        `/shared/${id}`,
        new Response(file, { headers: { "Content-Type": file.type || "application/octet-stream" } }),
      );
      target.searchParams.append("file", id);
    }
    for (const key of ["title", "text", "url"]) {
      const value = form.get(key);
      if (typeof value === "string" && value) target.searchParams.set(key, value);
    }
  } catch (error) {
    // Malformed or oversized bodies: still land the user in the app.
    console.error("Share target failed", error);
    target.searchParams.set("share-error", "1");
  }
  // 303 turns the POST into a GET, so a reload never re-submits the share.
  return Response.redirect(target.href, 303);
}

Gotchas

  • The app must be installed to appear in the share sheet, and the share sheet only reflects changes after the installed app's manifest (on Android, the WebAPK) is updated.
  • On Android, shared links usually arrive in the text field, not url; extract URLs from text.
  • Declare both MIME types and extensions in accept; operating systems differ in which one they match.
  • Validate shared input like any form submission. Full guide: Web Share Target.

file_handlers

Registers the installed app with the operating system as a handler for file types, so it appears in "Open with" menus and can become the default app.

Field Value
Type array of file handler objects
Valid values Objects with an action URL within scope and a non-empty accept map, plus optional name, icons and launch_type ("single-client" or "multiple-clients")
Default Empty list
Defined in Manifest Incubations (WICG)
Status Incubation
Support Chrome and Edge 102 on desktop operating systems. Not on Android, Safari or Firefox.

File handler members

Member Required Type Notes
action Yes string (URL) Within scope; the page that receives the files
accept Yes object Keys are MIME types (image/* wildcards allowed); values are lists of extensions starting with .
name No string Human-readable file-type name for OS surfaces
icons No array of image resources File-type icons (a document-style icon, not the app logo)
launch_type No string "single-client" (default): one launch with all files. "multiple-clients": one launch per file.

Processing and validation

  • Non-arrays are ignored (property 'file_handlers' ignored, type array expected.). An entry is dropped if it is not an object, if action is missing or outside scope (FileHandler ignored. Property 'action' is invalid.), or if accept is missing, not an object, or ends up empty (FileHandler ignored. Property 'accept' is invalid.).
  • Within accept, the specification skips a MIME type that does not parse or whose top-level type is not registered with IANA, and skips an extension list that is empty, contains a non-string, contains a string that does not start with ., or contains a string longer than 16 characters.
  • Chromium additionally accepts a single string instead of a list, rejects extensions that are just . or contain invalid characters, and caps the total number of extensions across all handlers at 300 (property 'accept': too many total file extensions, ignoring extensions starting from "…").
  • The specification requires both MIME types and extensions because Linux uses both, while Windows uses only extensions. It also notes that Windows never launches an application with several files at once, so on Windows every multi-file open behaves like multiple-clients.
  • Files arrive as FileSystemFileHandle objects through window.launchQueue (see launch_handler). Chrome asks the user to allow file handling the first time a file is opened with the app.
app.webmanifest
{
  "file_handlers": [
    {
      "action": "/open-board",
      "name": "Acme task board",
      "accept": { "application/vnd.acme.board+json": [".acmeboard"] },
      "icons": [{ "src": "/icons/file-board-256.png", "sizes": "256x256", "type": "image/png" }],
      "launch_type": "multiple-clients"
    },
    {
      "action": "/import",
      "accept": { "text/csv": [".csv"] }
    }
  ]
}

Gotchas

  • Prefer your own extensions over generic ones such as .json; users do not appreciate an app that claims common types, and the OS will not silently hand over an existing default.
  • Registering only opens the app; reading and writing the file is your code's job. Full guide: File handling. Chromium-specific behavior of icons and launch_type: Advanced & integration members.

protocol_handlers

Registers the installed app as a handler for URL schemes such as mailto: or a custom web+ scheme.

Field Value
Type array of objects
Valid values { "protocol", "url" } entries where protocol is a safelisted scheme or web+ followed by ASCII letters, and url is within scope and contains %s
Default Empty list
Defined in Manifest Incubations (WICG), reusing HTML's registerProtocolHandler() normalization rules
Status Incubation
Support Chrome and Edge 96 on desktop. Not on Android, Safari or Firefox.

Processing and validation

  • protocol is lowercased and must be one of HTML's safelisted schemes (bitcoin, ftp, ftps, geo, im, irc, ircs, magnet, mailto, matrix, mms, news, nntp, openpgp4fpr, sftp, sip, sms, smsto, ssh, tel, urn, webcal, wtai, xmpp) or web+ followed by one or more ASCII letters. A trailing colon ("mailto:") or digits and dashes ("web+app2", "web+my-app") make it invalid.
  • url is resolved against the manifest URL, must be http(s), same-origin and within scope, and must contain %s, which is replaced with the percent-encoded URL being handled.
  • Entries with a missing or invalid protocol or url are dropped with a specific console message, and duplicate URLs are skipped.
  • The specification says browsers "SHOULD ask users for permission" before registering the app as the OS default handler; Chrome asks on first use and lets the user revoke it. Isolated Web Apps may register a wider set of schemes.
app.webmanifest
{
  "protocol_handlers": [
    { "protocol": "web+tasks", "url": "/open?link=%s" },
    { "protocol": "mailto", "url": "/tasks/from-mail?address=%s" }
  ]
}

For web+tasks:board/42, the app launches at /open?link=web%2Btasks%3Aboard%2F42.

Gotchas

  • DevTools lists registered handlers in the Manifest pane and can test-launch them.
  • Parse the %s value defensively: any page or app on the system can craft these URLs. Full guide: Protocol handlers & launch handling.

note_taking

Identifies the app as a note-taking app and gives the operating system a URL for creating a new note.

Field Value
Type object
Valid values { "new_note_url": <URL within scope> }
Default None
Defined in Manifest Incubations (WICG)
Status Incubation
Support Chrome 95 (chromestatus: "Note taking new note URL"). Parsed on every Chromium platform, used only on ChromeOS.
  • note_taking must be an object (property 'note_taking' ignored, type object expected.). new_note_url is resolved against the manifest URL and ignored if it is outside scope.
  • The specification calls the member advisory. ChromeOS offers note-taking apps from its stylus tools and launches the app at new_note_url through the normal launch steps.
app.webmanifest
{
  "note_taking": {
    "new_note_url": "/notes/new?source=os"
  }
}

Gotcha: make the new_note_url page open instantly into an empty, focused editor, and make it work offline; a stylus user expects to write immediately.

Vendor-specific and non-standard members

edge_side_panel

Deprecated

Microsoft's documentation carries the notice "Update July 2026: This feature is being deprecated; it will soon no longer be supported." Do not build new features on it.

Opts the app into being pinned in the Microsoft Edge sidebar.

Field Value
Type object
Valid values {} or { "preferred_width": <number of CSS pixels> }
Default None
Defined in Microsoft Edge documentation
Status Vendor (Microsoft), deprecated
Support Microsoft Edge on desktop
  • The sidebar's default minimum width is 376 pixels and users can resize it. preferred_width sets the width Edge opens it at, but users can still shrink it to 376 pixels, so your layout must work there.
  • With "display": "browser" (or no display), the app can be pinned to the sidebar without being installable as a standalone app.
  • Detect the sidebar with the Edge Side Panel brand in the Sec-CH-UA header or navigator.userAgentData.brands, or the same token in the User-Agent string.
app.webmanifest
{
  "edge_side_panel": {
    "preferred_width": 480
  }
}

widgets

Experimental

widgets is a Microsoft-specific member for the Windows 11 Widgets Board, in apps installed from Microsoft Edge. It is not part of any specification, Microsoft's page for it has not been substantively revised since 2023, and no other browser implements it.

Defines widgets built from Adaptive Cards templates, rendered by the Windows 11 Widgets Board and updated from the app's service worker.

Field Value
Type array of widget definitions
Valid values Objects with the members below
Default None
Defined in Microsoft Edge documentation
Status Vendor (Microsoft)
Support Microsoft Edge on Windows 11

Widget definition members

Member Required Meaning
name Yes Widget title shown to users
short_name No Shorter alternative name
description Yes What the widget does, shown in the widget picker
tag Yes Identifier used from the service worker
template No Generic template name; documented as "currently only informational and not used"
ms_ac_template Yes URL of the Adaptive Cards template
data No URL returning the JSON data bound to the template
type No MIME type of data
screenshots Yes Images of the widget; platform accepts Windows and any; images larger than 1024 × 1024 are ignored
icons No Widget icons; the app's icons are used when absent; larger than 1024 × 1024 ignored
auth No Whether the widget requires authentication
update No Desired update frequency in seconds; nothing updates automatically, your service worker must do it
multiple No Allow multiple instances; defaults to true

The service worker gets a self.widgets object (getByTag(), getByInstanceId(), getByHostId(), matchAll(), updateByTag(), updateByInstanceId()) and the events widgetinstall, widgetuninstall, widgetresume and widgetclick. A worked service worker is in Advanced & integration members.

app.webmanifest
{
  "widgets": [
    {
      "name": "Today's tasks",
      "description": "Shows the tasks due today",
      "tag": "today",
      "ms_ac_template": "/widgets/today-template.json",
      "data": "/widgets/today-data.json",
      "type": "application/json",
      "screenshots": [{ "src": "/widgets/today.png", "sizes": "600x400", "label": "Today's tasks widget" }],
      "update": 3600
    }
  ]
}

serviceworker

A non-standard member for just-in-time installation of web-based payment handlers: when a merchant invokes a payment method whose payment app is not installed yet, Chromium can install the payment app's service worker from this member.

Field Value
Type object
Valid values src (service worker script URL), scope (registration scope), use_cache (boolean)
Default None
Defined in Chromium's payment handler implementation (non-standard)
Status Vendor (Chromium)
Support Chrome and Chrome for Android 70, Edge 79, Samsung Internet 10.0
  • use_cache maps to updateViaCache: true behaves like "imports", false like "none".
  • It has no effect on regular PWA installation, and the web app manifest parser that serves installation does not read it. Register your app's service worker in JavaScript. See Payments.
pay.webmanifest
{
  "name": "Acme Pay",
  "icons": [{ "src": "/icons/pay-192.png", "sizes": "192x192", "type": "image/png" }],
  "serviceworker": {
    "src": "/pay/sw.js",
    "scope": "/pay/",
    "use_cache": false
  }
}

gcm_sender_id

A legacy Chromium member that carried the Google Cloud Messaging sender ID for push notifications in early versions of Chrome's Push API support, before VAPID application server keys existed.

Field Value
Type string
Valid values A GCM sender ID
Default None
Defined in Chromium (legacy)
Status Vendor (obsolete)
Support Still parsed by Chromium; standard Web Push does not use it

Standard Web Push with VAPID keys needs no manifest member at all: you pass the public key to pushManager.subscribe(). Remove gcm_sender_id from old manifests.

Proposals, experiments and retired members

translations

Experimental

translations is only parsed when Chromium's experimental web platform features are enabled, and its chromestatus entry ("Web app translations") says "No active development". It has been superseded by the standardized *_localized members.

An earlier Chromium design for localization: a map from language tags to objects with translated name, short_name and description.

Field Value
Type object
Valid values Language tags mapped to { "name"?, "short_name"?, "description"? }
Default None
Defined in WICG explainer in the Manifest Incubations repository
Status Proposal, superseded
Support Behind a flag in Chromium
app.webmanifest
{
  "name": "Color Picker",
  "translations": {
    "fr": { "name": "Sélecteur de Couleur", "short_name": "Couleurs" }
  }
}

Other proposals, retired experiments and special-purpose members

Member or proposal What it did Status (September 2026) Use instead
user_preferences ("Dark mode support for web apps") Dark-mode theme_color and background_color Chromium origin trial from Chrome 99, extended through Chrome 114; not shipped color_scheme_dark (specified, unimplemented) plus <meta name="theme-color" media>
url_handlers ("Progressive Web Apps as URL Handlers") Let an app handle links to several origins chromestatus: "No longer pursuing" scope_extensions
capture_links ("Declarative Link Capturing for PWAs") Declarative link capturing into the app window chromestatus: "No longer pursuing" launch_handler plus user settings
lock_screen A separate start URL for note taking on the lock screen Chromium still parses the object, but the related Lock Screen API is marked "No active development" None
Scope inclusions and exclusions ("Web app manifest scope filtering") URL-pattern additions to and exclusions from scope chromestatus: "Proposed" (a Microsoft-led explainer) A scope ending in /
version, permissions_policy, update_manifest_url Versioning, permissions and self-updates for Isolated Web Apps Used by Isolated Web Apps only Not applicable to regular PWAs

Typing the manifest in TypeScript

If you generate the manifest in a build step (per environment, per tenant or per locale), a type definition catches the most common mistakes before a browser silently ignores them: enhanced modes in display, strings where booleans belong, misspelled keywords. The module below encodes the valid values documented on this page, using only type syntax that Node.js can strip at runtime, so the same file runs directly in a Node.js 22.18 (or 23.6) and later build script, without a compiler.

src/web-app-manifest.ts
/**
 * Web app manifest types, matching the processing rules browsers apply
 * (September 2026). Every member is optional because the specification makes
 * every member optional; comments note where an engine needs a member.
 */

export type TextDirection = "ltr" | "rtl" | "auto";

/** Values accepted by `display`. Anything else is ignored and "browser" applies. */
export type DisplayMode = "fullscreen" | "standalone" | "minimal-ui" | "browser";

/** Values accepted by `display_override` (Chromium). Unknown values are skipped. */
export type DisplayOverrideMode = DisplayMode | "window-controls-overlay" | "tabbed";

export type OrientationLock =
  | "any"
  | "natural"
  | "landscape"
  | "portrait"
  | "portrait-primary"
  | "portrait-secondary"
  | "landscape-primary"
  | "landscape-secondary";

type PurposeToken = "any" | "maskable" | "monochrome";

/** A space-separated set of purpose tokens, for example "monochrome" or "any maskable". */
export type IconPurpose = PurposeToken | `${PurposeToken} ${PurposeToken}`;

export interface ImageResource {
  /** Resolved against the manifest URL. */
  src: string;
  /** "192x192", "16x16 32x32" or "any". Must match the real pixels. */
  sizes?: string;
  /** MIME type hint, for example "image/png". */
  type?: string;
  /** Accessible name. */
  label?: string;
  /** Defaults to "any". An icon with no valid token is dropped. */
  purpose?: IconPurpose;
}

export interface Screenshot extends Omit<ImageResource, "purpose"> {
  /** Chrome shows "wide" on desktop and "narrow" (or unset) on Android. */
  form_factor?: "narrow" | "wide";
  /** Distribution platform, for stores. Chromium ignores it. */
  platform?: string;
}

export interface LocalizedText {
  value: string;
  lang?: string;
  dir?: TextDirection;
}

/** Keys are BCP 47 language tags, for example "de" or "fr-CA". */
export type LanguageMap<T> = Record<string, T>;
export type LocalizedTextMap = LanguageMap<string | LocalizedText>;

export interface ShortcutItem {
  /** Required and non-empty, or the shortcut is dropped. */
  name: string;
  /** Required; must be within scope. */
  url: string;
  short_name?: string;
  description?: string;
  icons?: ImageResource[];
  name_localized?: LocalizedTextMap;
  short_name_localized?: LocalizedTextMap;
  description_localized?: LocalizedTextMap;
  icons_localized?: LanguageMap<ImageResource[]>;
}

export interface ShareTargetFiles {
  /** Form field name. */
  name: string;
  /** MIME types and/or extensions. One invalid MIME type voids share_target in Chromium. */
  accept: string | string[];
}

export type ShareTarget =
  | {
      action: string;
      method?: "GET";
      enctype?: "application/x-www-form-urlencoded";
      params: { title?: string; text?: string; url?: string };
    }
  | {
      action: string;
      method: "POST";
      /** Required with POST by the specification; files need multipart/form-data. */
      enctype: "application/x-www-form-urlencoded" | "multipart/form-data";
      params: {
        title?: string;
        text?: string;
        url?: string;
        files?: ShareTargetFiles | ShareTargetFiles[];
      };
    };

export interface FileHandler {
  /** Must be within scope. */
  action: string;
  /** MIME type to extension list, for example { "text/csv": [".csv"] }. */
  accept: Record<string, string[]>;
  name?: string;
  icons?: ImageResource[];
  launch_type?: "single-client" | "multiple-clients";
}

export interface ProtocolHandler {
  /** A safelisted scheme ("mailto") or "web+" followed by letters. */
  protocol: string;
  /** Must be within scope and contain "%s". */
  url: string;
}

export type ClientMode = "auto" | "navigate-new" | "navigate-existing" | "focus-existing";

export interface RelatedApplication {
  platform: "play" | "windows" | "webapp" | "chrome_web_store" | (string & {});
  url?: string;
  id?: string;
  min_version?: string;
  fingerprints?: Array<{ type: string; value: string }>;
}

export interface ScopeExtension {
  type: "origin";
  /** An https origin that serves /.well-known/web-app-origin-association. */
  origin: string;
}

export type MigrateFromEntry =
  | string
  | { id: string; install_url?: string; behavior?: "suggest" | "force" };

export interface WebAppManifest {
  // Identity and naming. Chromium needs name or short_name to install.
  id?: string;
  name?: string;
  short_name?: string;
  description?: string;
  lang?: string;
  dir?: TextDirection;
  name_localized?: LocalizedTextMap;
  short_name_localized?: LocalizedTextMap;
  description_localized?: LocalizedTextMap;
  migrate_from?: MigrateFromEntry[];
  migrate_to?: { id: string; install_url?: string };

  // Launch and navigation. Chromium needs an explicit, same-origin start_url.
  start_url?: string;
  scope?: string;
  scope_extensions?: ScopeExtension[];
  launch_handler?: { client_mode?: ClientMode | ClientMode[] };

  // Presentation.
  display?: DisplayMode;
  display_override?: DisplayOverrideMode[];
  orientation?: OrientationLock;
  theme_color?: string;
  background_color?: string;
  color_scheme_dark?: { theme_color?: string; background_color?: string };
  tab_strip?: {
    home_tab?: { scope_patterns?: Array<string | Record<string, string>> };
    new_tab_button?: { url?: string };
  };

  // Imagery. Chromium needs an "any" icon of at least 144 px to install.
  icons?: ImageResource[];
  icons_localized?: LanguageMap<ImageResource[]>;
  screenshots?: Screenshot[];

  // Store and related-app metadata.
  categories?: string[];
  iarc_rating_id?: string;
  related_applications?: RelatedApplication[];
  prefer_related_applications?: boolean;

  // OS integration.
  shortcuts?: ShortcutItem[];
  share_target?: ShareTarget;
  file_handlers?: FileHandler[];
  protocol_handlers?: ProtocolHandler[];
  note_taking?: { new_note_url: string };
}

/** Identity function that gives you autocompletion and type errors. */
export function defineManifest<M extends WebAppManifest>(manifest: M): M {
  return manifest;
}
scripts/build-manifest.ts
// Run with: node scripts/build-manifest.ts production
import { mkdir, writeFile } from "node:fs/promises";
import { defineManifest } from "../src/web-app-manifest.ts";

const environment = process.argv[2] ?? "production";
const isProduction = environment === "production";

const manifest = defineManifest({
  id: "/", // Never varies by environment: a different id is a different app.
  name: isProduction ? "Acme Tasks" : `Acme Tasks (${environment})`,
  short_name: "Tasks",
  start_url: "/?source=pwa",
  scope: "/",
  display: "standalone",
  theme_color: "#0b57d0",
  background_color: "#ffffff",
  icons: [
    { src: "/icons/icon-192.png", sizes: "192x192", type: "image/png" },
    { src: "/icons/icon-512.png", sizes: "512x512", type: "image/png" },
    { src: "/icons/maskable-512.png", sizes: "512x512", type: "image/png", purpose: "maskable" },
  ],
});

try {
  await mkdir("dist", { recursive: true });
  await writeFile("dist/app.webmanifest", `${JSON.stringify(manifest, null, 2)}\n`);
  console.log(`Wrote dist/app.webmanifest for ${environment}`);
} catch (error) {
  console.error("Could not write the manifest", error);
  process.exitCode = 1;
}

Types stop only type errors. Rules that depend on URL resolution (same-origin start_url, start_url within scope, shortcut URLs within scope) need a real processing step: the validator script in Debugging manifests applies them in CI.

Complete example manifest

The manifest below uses every member that ships in at least one stable browser and applies to a regular PWA. It deliberately leaves out edge_side_panel (being deprecated), widgets (Windows-only, needs Adaptive Cards templates), migrate_from and migrate_to (only meaningful during an origin migration), color_scheme_dark (unimplemented), handle_links and translations (not shipped), serviceworker (payment handlers only) and gcm_sender_id (obsolete). Every relative URL assumes the file is served from https://app.example.com/app.webmanifest.

app.webmanifest (every stable member)
{
  "id": "/",
  "name": "Acme Tasks: Team To-Do Lists",
  "name_localized": {
    "de": "Acme Aufgaben: Team-To-do-Listen",
    "fr": { "value": "Acme Tâches : listes d'équipe", "lang": "fr" }
  },
  "short_name": "Tasks",
  "short_name_localized": {
    "de": "Aufgaben",
    "fr": "Tâches"
  },
  "description": "Plan, assign and track team tasks. Works offline and syncs when you reconnect.",
  "description_localized": {
    "de": "Teamaufgaben planen, zuweisen und verfolgen, auch offline.",
    "fr": "Planifiez, attribuez et suivez les tâches, même hors ligne."
  },
  "lang": "en-US",
  "dir": "ltr",
  "start_url": "/?source=pwa",
  "scope": "/",
  "scope_extensions": [
    { "type": "origin", "origin": "https://help.example.com" }
  ],
  "display": "standalone",
  "display_override": ["tabbed", "window-controls-overlay"],
  "tab_strip": {
    "new_tab_button": { "url": "/tasks/new" }
  },
  "orientation": "any",
  "theme_color": "#0b57d0",
  "background_color": "#ffffff",
  "icons": [
    { "src": "/icons/icon-192.png", "sizes": "192x192", "type": "image/png" },
    { "src": "/icons/icon-512.png", "sizes": "512x512", "type": "image/png" },
    { "src": "/icons/maskable-192.png", "sizes": "192x192", "type": "image/png", "purpose": "maskable" },
    { "src": "/icons/maskable-512.png", "sizes": "512x512", "type": "image/png", "purpose": "maskable" },
    { "src": "/icons/monochrome.svg", "sizes": "any", "type": "image/svg+xml", "purpose": "monochrome" }
  ],
  "icons_localized": {
    "ja": [
      { "src": "/icons/ja/icon-192.png", "sizes": "192x192", "type": "image/png" },
      { "src": "/icons/ja/icon-512.png", "sizes": "512x512", "type": "image/png" }
    ]
  },
  "screenshots": [
    {
      "src": "/shots/board-wide.png",
      "sizes": "1920x1080",
      "type": "image/png",
      "form_factor": "wide",
      "label": "Task board with To do, Doing and Done columns"
    },
    {
      "src": "/shots/today-narrow.png",
      "sizes": "1080x2340",
      "type": "image/png",
      "form_factor": "narrow",
      "label": "Today's tasks on a phone"
    }
  ],
  "shortcuts": [
    {
      "name": "New task",
      "name_localized": { "de": "Neue Aufgabe", "fr": "Nouvelle tâche" },
      "short_name": "New",
      "description": "Create a task in your inbox",
      "url": "/tasks/new?source=shortcut",
      "icons": [{ "src": "/icons/shortcut-new-96.png", "sizes": "96x96", "type": "image/png" }]
    },
    {
      "name": "Today",
      "url": "/tasks/today?source=shortcut",
      "icons": [{ "src": "/icons/shortcut-today-96.png", "sizes": "96x96", "type": "image/png" }]
    }
  ],
  "categories": ["productivity", "business"],
  "iarc_rating_id": "e84b072d-71b3-4d3e-86ae-31a8ce4e53b7",
  "related_applications": [
    {
      "platform": "play",
      "url": "https://play.google.com/store/apps/details?id=com.example.tasks",
      "id": "com.example.tasks"
    }
  ],
  "prefer_related_applications": false,
  "launch_handler": {
    "client_mode": ["focus-existing", "auto"]
  },
  "share_target": {
    "action": "/share-target",
    "method": "POST",
    "enctype": "multipart/form-data",
    "params": {
      "title": "title",
      "text": "text",
      "url": "url",
      "files": [{ "name": "media", "accept": ["image/png", "image/jpeg", ".png", ".jpg", ".jpeg"] }]
    }
  },
  "file_handlers": [
    {
      "action": "/open-board",
      "name": "Acme task board",
      "accept": { "application/vnd.acme.board+json": [".acmeboard"] },
      "launch_type": "multiple-clients"
    }
  ],
  "protocol_handlers": [
    { "protocol": "web+tasks", "url": "/open?link=%s" }
  ],
  "note_taking": {
    "new_note_url": "/tasks/new?type=note"
  }
}

Notes on the choices:

  • id: "/" with start_url: "/?source=pwa" keeps the identity stable while the query string counts launches for analytics.
  • display_override: ChromeOS gets a tabbed window; other Chromium desktops (where tabbed is not enabled) get Window Controls Overlay; Chromium on Android and every other engine fall back to display: standalone. If you do not want to build the custom title bar that the overlay requires, drop window-controls-overlay.
  • tab_strip has no home_tab, so the new tab button is shown and opens /tasks/new.
  • scope_extensions only takes effect once https://help.example.com/.well-known/web-app-origin-association lists the app's id, https://app.example.com/.
  • launch_handler routes every launch into the existing window, so the page must register a launchQueue consumer (see launch_handler).
  • iarc_rating_id is the Application Information Note's example value; replace it with your certificate or remove it.
  • prefer_related_applications: false is the default and is included only to show the member.

Browser support summary

Member Chrome / Edge (desktop) Chrome (Android) Safari (macOS) Safari (iOS / iPadOS) Firefox (Android) Samsung Internet
name, short_name, start_url ✅ 39 ✅ 39 ✅ 17 ✅ 11.3 ✅ 79 ✅ 4.0
scope ✅ 53 ✅ 53 ✅ 17 ✅ 11.3 ✅ 79 ✅ 6.0
id ✅ 96 ✅ 96 ✅ 17 ✅ 16.4 ❌ ✅ 17.0
description ✅ 88 ✅ 88 ❌ ❌ ❌ ✅ 15.0
lang, dir ⚠️1 ❌ ❌ ❌ ❌ ❌
*_localized ✅ 148 ❌ ❌ ❌ ❌ ❌
migrate_from, migrate_to ✅ 1502 ❌ ❌ ❌ ❌ ❌
scope_extensions ✅ 1393 ❌ ❌ ❌ ❌ ❌
launch_handler ✅ 110 ⚠️ 1104 ❌ ❌ ❌ ⚠️ 21.04
handle_links ❌ ❌ ❌ ❌ ❌ ❌
display ✅ 39 ✅ 39 ⚠️ 175 ⚠️ 11.35 ✅ 47 ✅ 4.0
display_override ✅ 89 ✅ 89 ❌ ❌ ❌ ✅ 15.0
orientation ⚠️6 ✅ 39 ❌ ❌ ✅ 79 ✅ 4.0
theme_color ✅ 46 ✅ 46 ✅ 17 ✅ 15 ✅ 79 ✅ 5.0
background_color ✅ 46 ✅ 46 ❌ ❌ ✅ 79 ✅ 5.0
color_scheme_dark ❌ ❌ ❌ ❌ ❌ ❌
tab_strip ⚠️ 1267 ❌ ❌ ❌ ❌ ❌
icons ✅ 39 ✅ 39 ⚠️ 178 ⚠️ 15.48 ✅ 79 ✅ 4.0
screenshots ✅ 108 ✅ 94 ❌ ❌ ❌ ⚠️9
categories ❌ ❌ ✅ 17.4 ❌ ❌ ❌
iarc_rating_id ❌ ❌ ❌ ❌ ❌ ❌
related_applications ⚠️10 ✅ 44 ❌ ❌ ❌ ✅ 4.0
prefer_related_applications ⚠️10 ✅ 44 ❌ ❌ ❌ ✅ 4.0
shortcuts ✅ 9611 ✅ 84 ✅ 17.4 ❌ ❌ ✅ 14.0
share_target ⚠️ 8912 ✅ 76 ❌ ❌ ❌ ✅ 12.0
file_handlers ✅ 102 ❌ ❌ ❌ ❌ ❌
protocol_handlers ✅ 96 ❌ ❌ ❌ ❌ ❌
note_taking ⚠️ 9513 ❌ ❌ ❌ ❌ ❌
edge_side_panel, widgets ⚠️14 ❌ ❌ ❌ ❌ ❌

Firefox on desktop has no manifest-based installation. Since Firefox 143 (September 2025), Firefox on Windows can pin sites to the taskbar as web apps; Mozilla's architecture notes say it reads the manifest when present (for example to pick the app icon), but MDN's data lists no member support there.

Support data as of September 2026. Versions come from MDN's browser-compat-data and browser release notes; where they disagree, the member's section says so. For live data see MDN's manifest reference and caniuse.com.

Common pitfalls

  • Putting enhanced display modes in display. window-controls-overlay and tabbed are only valid in display_override. In display they are ignored and browser applies, which also makes the app non-installable in Chromium.
  • Strings where booleans or arrays are expected. "prefer_related_applications": "false" is ignored, not interpreted, and "categories": "productivity" is not a list.
  • Changing start_url without id. Existing users keep the old app; new users get a second one.
  • Relative URLs in a cross-origin manifest. start_url, scope and every in-scope URL member break silently because they resolve against the manifest's origin.
  • A start_url outside scope. "start_url": "/app" with "scope": "/app/" discards the scope and falls back to the start URL's directory.
  • Unknown purpose tokens only. An icon whose purpose contains no valid token is discarded, not treated as any.
  • Only maskable icons. Chromium's installability check wants an any icon of at least 144 px.
  • Replacing icon files in place. Browsers following the current specification, including Chrome 144 and later, compare icon URLs, not bytes; change the URL.
  • Shortcut, share target or handler URLs outside scope. Each such entry is dropped with only a console message.
  • Too many shortcuts. Chromium stops at the tenth array entry, invalid ones included.
  • One bad MIME type in share_target.params.files. Chromium drops the entire share target.
  • Trusting lang to translate. It labels the default language; translations need *_localized or server-side negotiation.
  • Leaving legacy vendor members in place. gcm_sender_id does nothing today and edge_side_panel is on its way out; stale members mislead future maintainers.

Debugging members

  • Chrome and Edge DevTools → Application → Manifest shows the processed value of every member Chromium understands, grouped into Identity, Presentation, Protocol handlers, Icons, Shortcuts and Screenshots, followed by the installability result and the parser's errors and warnings. If a member you set shows no value there, look for its message in that errors list.
  • Console, filtered on Manifest: lists each rejected value with the messages quoted on this page, including line and column numbers for JSON syntax errors.
  • chrome://web-app-internals (desktop) shows what an installed app actually stored, which is what matters after an update; about://webapks does the same for Android WebAPKs.
  • Safari and Firefox show no per-member processing details. Test on devices, and remember that Safari reads apple-touch-icon before manifest icons.

More on the tools: Browser DevTools and the debugging section of the manifest overview.

Further reading

On this site

External references


  1. Parsed by Chromium. dir applies to shortcut text, and lang is the fallback language of *_localized entries (Chrome 148+). No other documented effect. ↩

  2. Chrome 150 release notes; MDN's data lists Chrome and Edge 149. Same-site origins only. ↩

  3. Chrome 139 release notes; MDN's data lists 138 and also Chrome for Android 138 and Samsung Internet 30.0, where the member is parsed but not applied. ↩

  4. The member is parsed, but window.launchQueue is not available on Android. ↩↩

  5. Only standalone and browser. ↩↩

  6. Parsed; only meaningful on devices whose screens rotate. ↩

  7. Shipped on ChromeOS only; behind flags on other desktops. ↩

  8. Only when no apple-touch-icon is present and purpose is any or absent. ↩↩

  9. MDN has no data for Samsung Internet. ↩

  10. Desktop Chromium uses the list for install-promotion decisions and getInstalledRelatedApps() (Windows apps since 85, installed web apps since 140). MDN lists Edge 17 and later. ↩↩

  11. Chrome and Edge 85–95 supported shortcuts on Windows only. ↩

  12. ChromeOS only; Chrome on Windows, macOS and Linux does not register share targets. ↩

  13. Used on ChromeOS only. ↩

  14. Microsoft Edge only; edge_side_panel is being deprecated and widgets requires Windows 11. ↩