Authors: Daniel Murphy dmurph@chromium.org Created: 2026-11-10
The launch_handler manifest field gives web apps control over app launches. Today, it handles single-window apps and arbitrary-many-window apps well. It does not support "multiple but limited" windows.
If developers emulate this using focus-existing and JavaScript, the wrong client activates first. Furthermore, the launch consumer receives no transient user activation (WICG/manifest-incubations#141). This prevents the consumer from focusing the correct window or opening a new one.
- Per-machine developer tool: A developer tool keeps exactly one window open per connected machine (e.g.,
/box/:machine/*). - Per-document editor: A document editor keeps exactly one window open per document ID.
- Per-workspace chat/mail: A chat app keeps exactly one window open per workspace.
- Per-account dashboards: A dashboard keeps exactly one window open per user account.
- Allow a web app to focus an existing window that matches a specific URL pattern.
- Create a declarative routing mechanism for app launches based on URL patterns.
- Choosing clients using arbitrary logic. Note that the Service Worker
launchevent is a separate, not yet implemented proposal. It is a potential future option, not something developers can use today. - Changing user-initiated new-window behavior.
We propose expanding the launch_handler dictionary to include a routes member. The routes member contains an ordered list of routing entries. Each entry contains a client_patterns array (using URL pattern strings or URLPatternInit objects) and a client_mode (a string or list of strings).
We also introduce a new client_mode value: focus-matching. The user agent treats top-level focus-matching without patterns as an unsupported mode and falls through to the next valid mode.
The user agent evaluates app scope (including scope inclusions, scope exclusions, and scope_extensions) before launch_handler. The launch targetURL is always already in scope. The routes match LaunchParams.targetURL for all launch triggers (links, shortcuts, file handlers, share targets, protocol handlers). The matched window receives the full LaunchParams. Matching origins from scope_extensions requires full-URL patterns or URLPatternInit objects.
At parse time, the user agent resolves the top-level client_mode to a single mode. It ignores invalid pattern entries in client_patterns. It drops any route whose client_patterns is empty. It also drops any route whose client_mode has no supported values. Valid routes retain their supported client_mode list for runtime evaluation. If no route matches at launch time, the user agent falls back to the top-level client_mode.
When a launch occurs, the user agent evaluates the routes list in order:
- It tests the patterns in
client_patternsagainst the launch URL in array order. - The first entry where the launch URL matches a pattern wins.
- The user agent applies the
client_modelist from the winning entry.
If the winning entry uses focus-matching, the user agent searches for a matching client. A "client" is a top-level application context with the manifest applied and an in-scope current URL. The user agent excludes regular browser tabs.
To define "matches" precisely:
- Find the first pattern in the winning entry's
client_patternsthat matches the candidate client's URL. If none match, the client does not match. - Use the first matching pattern for the launch URL. Use the first matching pattern for the candidate URL. Extract the matched named groups across all
URLPatterncomponents. The user agent leaves unmatched optional groups undefined. - If both sets of
[component, name, value]entries are identical under case-sensitive code-unit equality, the client matches. Zero named groups across both patterns matches any client matchingclient_patterns.
If one or more clients match, the user agent selects the most recently focused matching client. It brings that client into focus without navigating it. It then enqueues LaunchParams on the window.launchQueue of that client.
Patterns resolve relative to the manifest URL. If a pattern string or URLPatternInit omits an explicit search or hash component, the user agent sets that component's pattern to * before matching.
If focus-matching finds no matching client context, it falls through to the next supported mode in that entry's list. If none exist, it defaults to navigate-new. If the fallback mode is focus-existing or navigate-existing, the user agent picks the most recently used context regardless of URL.
Older user agents ignore unknown members. Because we keep launch_handler as a dictionary, old user agents ignore the routes array. They fall back to the top-level client_mode. The client_mode member has accepted either a string or a list of strings since the original explainer and spec. The user agent uses the first value it supports. Thus, the top-level client_mode remains the fallback for user agents that ignore routes. Inside routes[].client_mode, developers use the same string-or-list form. However, the user agent evaluates this list at runtime. It falls through when no client matches, rather than collapsing the list at parse time.
"launch_handler": {
"client_mode": "navigate-new",
"routes": [
{
"client_patterns": ["/box/:machine/*"],
"client_mode": "focus-matching"
}
]
}Note that if focus-matching finds no matching context, it defaults to navigate-new (the default fallback when no other mode is listed). Old user agents ignore routes and evaluate the top-level client_mode. The client_mode member has supported string lists since the original explainer. Developers can supply a list instead of a single string for any client_mode.
"launch_handler": {
"client_mode": ["navigate-existing", "auto"],
"routes": [
{
"client_patterns": ["/doc/:id/*"],
"client_mode": "focus-matching"
}
]
}This example demonstrates a non-default fall-through for the route.
"launch_handler": {
"client_mode": "focus-existing",
"routes": [
{
"client_patterns": ["/chat/:workspace/*"],
"client_mode": ["focus-matching", "navigate-existing"]
}
]
}For focus-matching, the user agent brings the target window into focus without navigating it. All modes enqueue LaunchParams. The target window receives the event via window.launchQueue and decides how to update its UI.
Web Platform Tests cannot install apps or trigger launches.
For the long-term path, we propose a test_driver extension or WebDriver BiDi command for "launch web app with URL".
In the interim, user agents will rely on their own browser-level integration tests. These tests must cover route selection order, named-group matching, and zero-named-group routes. They must also cover no-match fall-through, parse-time dropping of invalid routes, and old-UA behavior with routes present.
For developer ergonomics, we recommend browser DevTools add support for launch routing. Showing which route matched a given launch in the Application panel is a nice-to-have feature.
- Service worker
launchevent: This is powerful. However, a service worker adds registration and lifecycle overhead for static URL routing. Additionally, the launch event has not shipped. focus-matching-urlexact-URL mode: Proposed previously as a sketch. Exact URL matching fails for paths with sub-routes (e.g.,/doc/:id/editvs/doc/:id/view).- Per-launch-method launch_handler: Proposed in the existing explainer. It does not solve the URL-based routing problem.
- Separate manifests/scopes per workspace: Forces developers to manage multiple installations. This degrades the user experience.
- App-side BroadcastChannel routing: The wrong window activates first via
focus-existing. It cannot bring the correct window to the front because it lacks user activation.
- No new cross-origin exposure. Patterns only match within the app's scope.
- This proposal does not affect user "open in new window" gestures.
- No activation changes in this proposal. Developers must address the separate activation gap (WICG/manifest-incubations#141) separately.
For Chromium, this requires adding a URL filter on the existing most-recently-used client lookup in FindTopLevelBrowsingContextForWebApp. MRU tracking already exists.
- Tabbed display mode: Should matching consider all tabs within tabbed app windows, or just the active tab?