This document is intended as a starting point for engaging the community and standards bodies in developing collaborative solutions fit for standardization. As the solutions to problems described in this document progress along the standards-track, we will retain this document as an archive and use this section to keep the community up-to-date with the most current standards venue and content location of future work and discussions.
clients IntegrationToday, web developers lack a reliable way to determine whether their app is running in an installed app window, because the existing display-mode media queries conflate installation state with presentation mode and break under common scenarios like entering fullscreen. The application-context CSS media feature solves this by providing a stable, dedicated signal for the application context that is independent of the app's current display mode.
Developers would like a way to style their content differently depending on whether their web app is running in an installed app window. Common use cases include:
display-mode ProblemCurrently, the best available signal is the display-mode media query:
@media (display-mode: standalone) {
.install-banner { display: none; }
}
This works only until the app enters fullscreen. When a user triggers fullscreen, the display mode changes from standalone to fullscreen, and the media query no longer matches. In the example above, the install banner reappears, layout shifts, and app-specific UI disappears. The app is still installed, but the presentation no longer reflects this.
The two concepts of "is this an installed app?" and "what is the current display mode?" are orthogonal and should be treated as such. A web app can be installed and rendered in standalone, fullscreen, or minimal-ui mode. Developers need a signal that remains stable across all of these states.
@media rules enable reactive styling, while matchMedia() enables JavaScript-driven logic.navigator.standalone cross-browser. WebKit can keep its existing behavior; other engines should not adopt it. See Alternatives Considered.display-mode media queries. display-mode remains useful for adapting to presentation changes. application-context complements it.application-context. A solution would need to be generalized across all media query types and is outside the scope of this proposal.A new CSS media feature named application-context, used as an enumerated media feature with discrete values:
@media (application-context: installed) {
/* Styles applied only inside an installed app window */
}
@media (application-context: browser) {
/* Styles applied only in a regular browser tab */
}
The name application-context communicates that the feature describes the context in which the application is running, not whether the app is installed on the device globally.
installed when the document is in an application context: a top-level browsing context with a manifest applied, presented in its own OS-level app window.installed regardless of display mode. Whether the app is in standalone, fullscreen, or minimal-ui mode, the application-context media feature continues to match installed.browser. Same-origin iframes inherit the top-level context, since they already have access to the top-level window via window.top.browser in browser tabs. Even if the same URL has an installed app elsewhere, opening it in a regular browser tab means (application-context: installed) does not match. The feature reflects the current browsing context, not global installation state.matchMedia(). JavaScript can query and listen for changes using window.matchMedia(), following standard media query semantics.| Context | (application-context: installed) |
(display-mode: standalone) |
|---|---|---|
| Browser tab | no match | no match |
| Installed, standalone mode | match | match |
| Installed, then goes fullscreen | match | no match |
| Installed, minimal-ui mode | match | no match |
| Same-origin iframe inside app window | match | no match |
| Cross-origin iframe inside app window | no match | no match |
The key benefit of this approach over the existing display-mode media query is that it provides a consistent signal for the application context, regardless of the current display mode of the window.
A PWA shows an install banner to browser-tab users but hides it for users already in the installed experience:
.install-banner {
display: flex;
}
@media (application-context: installed) {
.install-banner {
display: none;
}
}
Today, this breaks when the user enters fullscreen, and the banner flashes back. With application-context, the banner stays hidden.
An installed app shows a back button and "open in browser" link that don't make sense in a tab:
.app-nav {
display: none;
}
@media (application-context: installed) {
.app-nav {
display: flex;
}
}
A site conditionally shows a service worker update prompt only in the installed experience:
if (window.matchMedia('(application-context: installed)').matches) {
showUpdatePrompt();
}
Although uncommon, a document could transition between contexts (e.g., a browser tab being "captured" into an app window). Developers can listen for this reactively:
window.matchMedia('(application-context: installed)').addEventListener('change', (e) => {
document.body.classList.toggle('is-installed', e.matches);
});
installed)An alternative approach is to define a boolean media feature named installed:
@media (installed) {
/* Styles for an installed app window */
}
@media not (installed) {
/* Styles for a regular browser tab */
}
This design is simpler to author, following the pattern of other boolean media features like (hover) or (scripting). However:
installed suggests a statement about global installation state. A developer might reasonably expect (installed) to be true if the app is installed on the device, even when viewed in a browser tab. In reality, the feature would only match when running inside an installed app window. The name application-context makes this distinction explicit, and describes the current context, not a global property.application-context can grow by adding new values.browser counterpart. With a boolean feature, styling for the browser-tab case requires not (installed), which is less readable and less intentional than (application-context: browser).Conclusion: While the boolean form is simpler for a binary state check, the application-context enumerated approach offers clearer semantics and room to grow.
navigator.standalonenavigator.standalone has been historically supported on WebKit. It returns true when a page is displayed in standalone mode. However, the property has become a de facto method for detecting iOS/iPad rather than detecting installed apps:
'standalone' in navigator as a platform-detection signal for iOS/iPads, not to detect installed apps. Standardizing this property across Chrome, Firefox, and others would cause those checks to fire on all platforms, massively amplifying breakage. When Safari 17 brought navigator.standalone to macOS desktop, Mozilla was using platform === 'MacIntel' && 'standalone' in navigator to identify iPads, sending desktop Mac users to the iOS App Store. While it was fixed, this exposed ambiguous semantics that don't directly match to installation and causes web compatibility issues.display-mode value in the Web App Manifest spec. Reusing it as a navigator property name conflates two different concepts (installation state and display mode) making developer intent ambiguous.Conclusion: navigator.standalone is unsuitable as a cross-browser standard. WebKit can maintain its proprietary behavior which is used to identify iOS devices; other engines should not adopt it. A CSS media feature like application-context avoids all of these issues.
display-mode with a New ValueOne option is adding a value like display-mode: installed. However:
display-mode is designed to reflect how the content is presented, not whether it is installed. Adding an installation signal conflates the two concepts.display-mode: fullscreen, not display-mode: installed, so the problem remains unsolved.display-mode: standalone and display-mode: installed.A related idea is a compound value like display-mode: standalone-fullscreen, representing a state where the app is both standalone and fullscreen. There are similarities here to how window-controls-overlay seems to extend standalone mode. An author could then write an OR query like @media (display-mode: standalone) or (display-mode: standalone-fullscreen) to cover both states. However, while window-controls-overlay is an appropriate extension of display-mode because it describes a visible presentation change (the title bar area has developer-mutable elements), installation state is not a presentation change — it does not alter how content is visually rendered. Encoding it into display-mode still conflates "is this app installed?" with "how is this app displayed?"
Conclusion: A separate media feature is the correct design. The display-mode media feature should remain focused on visible presentation differences. Installation state is orthogonal to how content is displayed, and a dedicated signal like application-context cleanly represents this without overloading display-mode with non-presentational semantics.
navigator.isInstalled)A dedicated JS property could work, but:
matchMedia(), so no separate API is needed.Conclusion: The CSS media feature, accessible via matchMedia(), covers both CSS and JS use cases with a single mechanism.
(application-context: installed) as non-matching, even if the user has the app installed.display-mode: standalone, except that application-context is stable across display mode changes. It does not expose any new bits of entropy beyond what the user has already disclosed by opening the app window.browser in cross-origin iframes, preventing embedded third-party content from detecting the host app's installation state. Same-origin iframes are permitted to inherit the top-level context, as they already have full access to the top-level window via window.top and do not represent a privacy boundary.clients IntegrationService workers run in a separate thread from the page and act as a network proxy and event handler for the web app. They have no access to the DOM and therefore cannot use CSS media queries or window.matchMedia(). Instead, service workers interact with their controlled pages through the Clients API, which provides a list of Client objects representing each window, tab, or worker controlled by the service worker.
Today, each Client exposes properties such as url, id, type, and frameType, but it does not indicate whether the client is running in an installed app window or a regular browser tab. Currently, there is no direct way for a service worker to distinguish between these contexts. Developers resort to workarounds like message-passing from the page to the service worker to relay installation state, which is fragile, asynchronous, and not always timely. It is worth noting that this limitation is not unique to application-context, service workers cannot access any media query state for their clients. A comprehensive solution would need to generalize across all media feature types, which is beyond the scope of this proposal. The following is included to illustrate the problem space and a possible future direction.
Without a built-in property on Client, developers would manually relay the app context from the page to the service worker using postMessage. This typically involves the page detecting its own context (via matchMedia) on load and sending a message to the service worker, which then maintains a mapping of client IDs to their contexts:
Page (client-side):
// On page load, inform the service worker of the current app context
if (navigator.serviceWorker.controller) {
const isInstalled = window.matchMedia('(application-context: installed)').matches;
navigator.serviceWorker.controller.postMessage({
type: 'app-context-report',
context: isInstalled ? 'installed' : 'browser'
});
}
Service Worker:
// Maintain a map of client contexts reported by pages
const clientContexts = new Map();
self.addEventListener('message', (event) => {
if (event.data?.type === 'app-context-report') {
clientContexts.set(event.source.id, event.data.context);
}
});
// Later, when deciding how to handle a push notification:
self.addEventListener('push', async (event) => {
const allClients = await self.clients.matchAll({ type: 'window' });
const installedClient = allClients.find(
client => clientContexts.get(client.id) === 'installed'
);
if (installedClient) {
installedClient.postMessage({ type: 'update-available' });
} else {
self.registration.showNotification('New update available');
}
});
This approach has several drawbacks:
matchMedia changes and send follow-up messages, adding further complexity.push event) before the page has had a chance to send its context report.A natural complement to the app-context CSS media feature would be exposing the same information on the WindowClient interface in the Service Worker API. For example, an appContext property:
const allClients = await self.clients.matchAll({ type: 'window' });
const installedClient = allClients.find(client => client.appContext === 'installed');
if (installedClient) {
// An installed app window exists — post a message to it
installedClient.postMessage({ type: 'update-available' });
} else {
// No installed client — show a system notification
self.registration.showNotification('New update available');
}
This extension would align the service worker's view of its clients with the information already available to pages via the application-context media feature, closing the gap in contexts where DOM-based detection is not possible. The exact shape of this API (property name, semantics for non-window clients, etc.) is left for future discussion and would be specified alongside the Service Worker and Clients API standards.
Many thanks for valuable feedback and advice from:
References: