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
*_localizedfamily andcolor_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,idandscopedepend on each other. Set all three explicitly, and setidbefore you ever changestart_url.- URL-valued members resolve against the manifest URL (
idis the exception: it resolves against the origin ofstart_url). Navigational URLs must be same-origin with the document and usually withinscope. name,short_nameandicons, and their*_localizedforms, 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 insidedisplay_override. Indisplaythey are ignored and the defaultbrowserapplies.
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,orientationanddirbefore 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.webmanifestturns"start_url": "home"into/static/home. Always use root-relative paths. The details, including what happens with cross-origin anddata: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
nameis missing, empty or the wrong type, the specification's "application's name" rules let the browser useshort_nameinstead, 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
nameorshort_nameand reports "Manifest does not contain a 'name' or 'short_name' field" otherwise. Firefox for Android's parser usesname, falls back toshort_name, and rejects a manifest with neither. - Security-sensitive: the specification asks browsers to apply a changed
nameonly 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.
Gotchas
- In standalone windows Chromium prepends
short_name(ornamewhen there is noshort_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:
nameis used when the app is installed, andshort_name"on the user's home screen, launcher, or other places where space is limited". - Security-sensitive, like
name.
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_nameas the window-title prefix, ashort_namelike "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 (kMaximumDescriptionLengthin Chromium). - Localizable in Chrome and Edge 148+ through
description_localized(see*_localized).
{
"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.
descriptionalso 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:
- Set
idto the processedstart_url. - If the value is not a string, or is the empty string, stop.
- Parse it with the origin of
start_urlas the base URL (not the manifest URL, and notstart_urlitself). - If parsing fails, or the result is not same-origin with
start_url, stop. - 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.
Gotchas
- Treat
idas 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 currentstart_url, query string included. Copy the Computed App Id from Chrome DevTools (Application → Manifest → Identity) intoidverbatim: astart_urlof/?source=pwaneeds"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=2are four different apps. - WebKit added
idin 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.
Gotchas
langdoes 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
langto match each variant, and keepididentical 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
dirto"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.
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 stringvalue;langanddirare optional overrides, and an invaliddiris ignored. An entry without a usablevalueis dropped. - Chromium differs from the specification in two details: an entry without its own
langgets the manifest-levellang(not the key), and an entry without its owndirgetsauto(not the manifest'sdir). 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 triesfr-CA, thenfr, then the default member. icons_localizedentries are complete replacements. When a locale matches, only its icons are used; they are not merged withicons.- 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".
{
"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_localizedvariant, or users of that locale lose the large icons (and a variant without a 144 px or largeranyicon can break Chromium's installability check for them). - The entry-level
langexists 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
*_localizedis 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_fromis only processed when the new manifest has an explicit, validid. Otherwise Chromium logsproperty 'migrate_from' ignored, manifest must specify an 'id' property in order to receive a migration.- Unlike the top-level
id, the ids insidemigrate_fromandmigrate_toare 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.), andinstall_url, when present, must be same-origin with the entry'sid. install_urlpoints 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.behavioris 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-associationwith an entry keyed by the new app'sidcontaining"allow_migration": true. Without that file, a hostile subdomain could claim someone else's installation. migrate_toin the old manifest is an optional, proactive signal. Its target's manifest must list the old app inmigrate_fromfor anything to happen.- Chrome does not offer migration for apps force-installed through the
WebAppInstallForceListenterprise policy; it shows an explanatory banner instead.
Gotchas
- Cross-site moves (
example.comtoexample.net) are refused by design. - Write the ids inside
migrate_fromandmigrate_toas absolute URLs. A relative value resolves against the manifest URL, not against the origin as the top-leveliddoes. - Keep the old origin serving its manifest (or at least the
install_urlpage) 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
- Set
start_urlto the document URL. - If the value is missing, not a string, or empty, stop.
- Parse it with the manifest URL as the base; stop on failure (Chromium:
property 'start_url' ignored, URL is invalid.). - If the result is not same-origin with the document URL, stop (Chromium:
property 'start_url' ignored, should be same origin as document.). - 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.
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 ashas_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 fromstart_url, so editingstart_urlcreates a new app. Setidfirst. start_urlmust be withinscope, or thescopemember is discarded.- Never encode user identity in it. The specification calls identifiers such as
?user=123a fingerprint "that is not cleared when the user clears site data". A generic marker such as?source=pwafor 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
- Set
scopeto"."resolved againststart_url(its directory). - If the value is the empty string, stop. (Chromium also ignores a non-string with
property 'scope' ignored, type string expected.) - Parse it with the manifest URL as the base; stop on failure.
- Remove the query and fragment.
- If
start_urlis not within the parsed scope, stop (Chromium:property 'scope' ignored. Start url should be within scope of scope URL.). - 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.
Gotchas
- End the scope with
/, as the prefix table shows. - A
start_urlof/appis not within a scope of/app/, so that combination silently discards yourscopeand 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
scopecovers one origin and one path prefix. Usescope_extensionsfor 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 bothtypeandorigin(scope_extensions entry ignored, required properties 'type' and 'origin' are missing.); onlytype: "origin"is defined. originmust parse as anhttpsorigin; 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 processedidURL) and whose values may contain ascopepath, 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.
Gotchas
- The key in the association file must equal the processed
idexactly (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_handlersorprotocol_handlersmust still be within the manifest's ownscope. - 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_handlermust be an object (Chromium:launch_handler value ignored, object expected.).client_modemay 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 withclient_mode value '…' ignored, unknown value.; if none is valid,autoapplies.- 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_handleris the developer control. - Since Chrome 146,
LaunchParams.targetURLis populated for file-handling launches directed at an existing window (it used to benull), and a page reload no longer re-delivers the previousLaunchParams.
// 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-existingdiscards whatever the user had open in that window; preferfocus-existingplus alaunchQueueconsumer for editors, players and chat apps.- Set the consumer early during startup. Launch parameters are queued until a consumer exists, and a
focus-existinglaunch with no consumer just focuses the window. - Chromium's parser reads only
client_mode. Fields from the 2021–2022 origin trial, such asroute_to, are ignored, so manifests copied from old articles silently getauto. - Full API details: Protocol handlers & launch handling and Advanced & integration members.
handle_links¶
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.
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
browserapplies (Chromium:unknown 'display' value ignored.). - Chromium explicitly rejects the enhanced modes here:
"display": "window-controls-overlay"or"tabbed"logsinapplicable 'display' value ignored.Those belong indisplay_override. - To choose the mode, the browser first gives other specifications a chance (that is where
display_overridehooks in), then usesdisplayif it supports it, then walks its fallback chain. Every browser must supportbrowser, so the chain always ends somewhere. - The applied mode, not the requested one, is what the
display-modeCSS media feature reports. - Chromium only treats a page as installable when the effective mode is
standalone,fullscreenorminimal-ui, or an enhanced mode fromdisplay_override. Firefox for Android requires anything other thanbrowser.
Gotchas
- The
fullscreendisplay mode is independent of the Fullscreen API: an app can be infullscreenmode whiledocument.fullscreenElementisnull. - On iOS and iPadOS, MDN's compatibility notes record that a Home Screen web app with
"display": "standalone"matchesdisplay-mode: fullscreenrather thanstandalone(WebKit bug 264218); a web app added without a manifest reportsbrowser, and a manifest"display": "fullscreen"opens as standalone. Checknavigator.standalone === truefirst, 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
displaysays; 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: fullscreenmedia 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
tabbedentries unless its tab-strip feature is enabled for the platform, and dropsunframedunless 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 acceptsurl_patternsonly forunframedand otherwise logsdisplay override '…' ignored, url_patterns are not allowed. - The first supported entry wins. If none is supported,
displayis evaluated with its normal fallback chain. web.dev's guidance adds that when there is nodisplaymember, the browser ignoresdisplay_overridealtogether, so always keepdisplay. - 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'".
{
"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 owndisplay_overrideexample) is silently skipped. Check the Presentation section in DevTools. - Do not put
browserfirst casually: as the first recognized entry it makes the app non-installable in Chromium. window-controls-overlaygives 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
browserdisplay mode. - At runtime,
screen.orientation.lock()can change it temporarily where the browser allows locking, andscreen.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 |
Gotchas
- Locking a phone app to portrait also locks it on tablets and foldables, where it can end up letterboxed. Prefer
anyplus 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()orcolor(display-p3 …)can be converted "without outside knowledge";color(--custom-profile …)cannot, because it needs an@color-profilerule 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 supportprefers-color-scheme.
Gotchas
- The manifest has no working dark variant yet (
color_scheme_darkis unimplemented). Use two<meta name="theme-color">tags withmedia="(prefers-color-scheme: …)"; Chrome honors themediaattribute 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_colorand 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 whetherbackground_coloris dark.
Gotchas
- Match your CSS
bodybackground 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_colorproduces a bright flash on every launch on Android. Untilcolor_scheme_darkships, 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.
{
"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_overrideselectstabbed. - 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.
{
"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
iconsis not an array, the whole member is ignored (property 'icons' ignored, type array expected.). Entries that are not objects or have no parsablesrcare dropped. purposeis 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 asany, 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.whensizescontains 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 CSPimg-srcdirective, 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, declaredsizesof at least 144 px (orany), 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 purposeanyormaskable. - 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 theiconsentries change, and applies changes of less than 10% (pixel comparison) without asking.
{
"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
maskableicons fails Chromium's installability check, which looks specifically for purposeany. - 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-schemeafterwards. Safari 26 accepts SVG icons (anddata:URLs for icons) too. Keep PNG fallbacks. - Declared
sizesmust match the real pixel dimensions: browsers pick icons by the declared size and then scale the actual pixels, so a 96 px file declared as512x512becomes 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 validsrcare dropped. - Chromium matches
form_factorcase-insensitively. An invalid value logsproperty '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
widescreenshots.
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 |
{
"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.
Store and related-app metadata¶
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.
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)".
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.
related_applications¶
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 withoutplatform('platform' is a required field, related application ignored.) and entries with neither a validurlnor anid(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 itswindows.appUriHandlerdeclaration.- Chromium suppresses its own install promotion when a related non-web app from this list is already installed (for example the
playpackage on Android), whateverprefer_related_applicationssays.
{
"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.
prefer_related_applications¶
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 defaultfalseapplies. - When
trueand 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, andbeforeinstallpromptdoesn't fire for it. - On Android with a
playentry, Chrome instead queries Google Play and offers the native app:beforeinstallpromptfires withplatformsset to["play"]. If the entry'surlcontains anid=parameter it must equalid, 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".
{
"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-emptyname(property 'name' of 'shortcut' not present.), has no stringurl, or itsurlis 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_handlerapplies. - Like
start_url, shortcut URLs must not carry user identifiers; the specification's privacy section calls that fingerprinting.
{
"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.
{
"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"] }
]
}
}
}
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
textfield, noturl; 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, ifactionis missing or outside scope (FileHandler ignored. Property 'action' is invalid.), or ifacceptis 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
FileSystemFileHandleobjects throughwindow.launchQueue(seelaunch_handler). Chrome asks the user to allow file handling the first time a file is opened with the app.
{
"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
iconsandlaunch_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
protocolis 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) orweb+followed by one or more ASCII letters. A trailing colon ("mailto:") or digits and dashes ("web+app2","web+my-app") make it invalid.urlis resolved against the manifest URL, must behttp(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
protocolorurlare 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.
{
"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
%svalue 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_takingmust be an object (property 'note_taking' ignored, type object expected.).new_note_urlis 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_urlthrough the normal launch steps.
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_widthsets 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 nodisplay), the app can be pinned to the sidebar without being installable as a standalone app. - Detect the sidebar with the
Edge Side Panelbrand in theSec-CH-UAheader ornavigator.userAgentData.brands, or the same token in theUser-Agentstring.
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.
{
"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_cachemaps toupdateViaCache:truebehaves like"imports",falselike"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.
{
"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 |
{
"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.
/**
* 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;
}
// 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.
{
"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: "/"withstart_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 (wheretabbedis not enabled) get Window Controls Overlay; Chromium on Android and every other engine fall back todisplay: standalone. If you do not want to build the custom title bar that the overlay requires, dropwindow-controls-overlay.tab_striphas nohome_tab, so the new tab button is shown and opens/tasks/new.scope_extensionsonly takes effect oncehttps://help.example.com/.well-known/web-app-origin-associationlists the app's id,https://app.example.com/.launch_handlerroutes every launch into the existing window, so the page must register alaunchQueueconsumer (seelaunch_handler).iarc_rating_idis the Application Information Note's example value; replace it with your certificate or remove it.prefer_related_applications: falseis 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-overlayandtabbedare only valid indisplay_override. Indisplaythey are ignored andbrowserapplies, 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_urlwithoutid. Existing users keep the old app; new users get a second one. - Relative URLs in a cross-origin manifest.
start_url,scopeand every in-scope URL member break silently because they resolve against the manifest's origin. - A
start_urloutsidescope."start_url": "/app"with"scope": "/app/"discards the scope and falls back to the start URL's directory. - Unknown
purposetokens only. An icon whosepurposecontains no valid token is discarded, not treated asany. - Only maskable icons. Chromium's installability check wants an
anyicon 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
langto translate. It labels the default language; translations need*_localizedor server-side negotiation. - Leaving legacy vendor members in place.
gcm_sender_iddoes nothing today andedge_side_panelis 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://webapksdoes the same for Android WebAPKs.- Safari and Firefox show no per-member processing details. Test on devices, and remember that Safari reads
apple-touch-iconbefore manifest icons.
More on the tools: Browser DevTools and the debugging section of the manifest overview.
Further reading¶
On this site
- Web App Manifest overview: fetching, processing, legacy tags and debugging
- App identity & updates:
id,start_url,scopeand the update pipelines - Icons & maskable icons
- Display modes
- Rich install UI:
descriptionandscreenshots - App shortcuts
- Advanced & integration members: handlers,
launch_handler,scope_extensions, Edge members - Manifest cheat sheet
External references
- Web Application Manifest (W3C Working Draft) and the editor's draft
- Web App Manifest – Application Information (W3C Group Note)
- Manifest Incubations (WICG)
- Web App Launch Handler API (WICG)
- Web Share Target API
- MDN: Web app manifest reference
- Chrome for Developers: A better way to update your web apps
- Chrome for Developers: Tabbed application mode
- Chrome for Developers: Launch Handler API
- WebKit: Features in Safari 17.4 (
shortcutsandcategorieson macOS) - Microsoft Edge: Build a PWA for the sidebar and Display a PWA widget in the Windows Widgets Board
-
Parsed by Chromium.
dirapplies to shortcut text, andlangis the fallback language of*_localizedentries (Chrome 148+). No other documented effect. ↩ -
Chrome 150 release notes; MDN's data lists Chrome and Edge 149. Same-site origins only. ↩
-
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. ↩
-
The member is parsed, but
window.launchQueueis not available on Android. ↩↩ -
Parsed; only meaningful on devices whose screens rotate. ↩
-
Shipped on ChromeOS only; behind flags on other desktops. ↩
-
Only when no
apple-touch-iconis present andpurposeisanyor absent. ↩↩ -
MDN has no data for Samsung Internet. ↩
-
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. ↩↩ -
Chrome and Edge 85–95 supported shortcuts on Windows only. ↩
-
ChromeOS only; Chrome on Windows, macOS and Linux does not register share targets. ↩
-
Used on ChromeOS only. ↩
-
Microsoft Edge only;
edge_side_panelis being deprecated andwidgetsrequires Windows 11. ↩