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.
- This document status: Active
- Expected venue: W3C CSS Working Group
- Current version: this document
- Introduction
- Goals
- Non-goals
- Proposed Solution
- Key Scenarios
- Alternatives Considered
- Privacy and Security Considerations
- Future Extension: Service Worker
clientsIntegration - References & Acknowledgements
Today, 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:
- Hiding install prompts when the user is already in the installed experience.
- Showing app-specific UI (e.g., a title bar, back navigation, or "open in browser" link) that only makes sense in an app window.
- Adapting layout for the app window's chrome (or lack thereof).
Currently, 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.
- Provide a stable signal for "running in an installed app window" that is separate from display mode, with clear naming that communicates the current application context rather than global installation state.
- Provide both declarative and imperative access to the signal. CSS
@mediarules enable reactive styling, whilematchMedia()enables JavaScript-driven logic.
- Standardize
navigator.standalonecross-browser. WebKit can keep its existing behavior; other engines should not adopt it. See Alternatives Considered. - Replace
display-modemedia queries.display-moderemains useful for adapting to presentation changes.application-contextcomplements it. - Expose media query state to service workers. The inability to access media queries from a service worker is a general limitation that affects all media features, not just
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.
- Matches
installedwhen the document is in an application context: a top-level browsing context with a manifest applied, presented in its own OS-level app window. - Remains
installedregardless of display mode. Whether the app is instandalone,fullscreen, orminimal-uimode, theapplication-contextmedia feature continues to matchinstalled. - Only applies to top-level browsing contexts and same-origin iframes. In cross-origin iframes, the feature matches
browser. Same-origin iframes inherit the top-level context, since they already have access to the top-level window viawindow.top. - Matches
browserin 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. - Usable via
matchMedia(). JavaScript can query and listen for changes usingwindow.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);
});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:
- Naming ambiguity. The name
installedsuggests a statement about global installation state. A developer might reasonably expect(installed)to betrueif 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 nameapplication-contextmakes this distinction explicit, and describes the current context, not a global property. - Limited extensibility. A boolean feature can only express two states. If future application contexts emerge, a boolean feature cannot accommodate them without introducing additional media features. An enumerated feature like
application-contextcan grow by adding new values. - No
browsercounterpart. With a boolean feature, styling for the browser-tab case requiresnot (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.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:
- Web compatibility risk. Countless sites use
'standalone' in navigatoras 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 broughtnavigator.standaloneto macOS desktop, Mozilla was usingplatform === 'MacIntel' && 'standalone' in navigatorto 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. - Naming confusion. The term "standalone" is already defined as a
display-modevalue 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.
One option is adding a value like display-mode: installed. However:
display-modeis designed to reflect how the content is presented, not whether it is installed. Adding an installation signal conflates the two concepts.- An app in fullscreen would still report
display-mode: fullscreen, notdisplay-mode: installed, so the problem remains unsolved. - Media features can only match one value at a time, you cannot simultaneously match
display-mode: standaloneanddisplay-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.
A dedicated JS property could work, but:
- CSS media features are already bridged to JS via
matchMedia(), so no separate API is needed. - A CSS-first approach gives developers reactive, declarative styling without requiring JavaScript.
Conclusion: The CSS media feature, accessible via matchMedia(), covers both CSS and JS use cases with a single mechanism.
- No cross-site information leak. The feature only reflects the current browsing context. It does not reveal whether the app is installed on the device, only whether the current document is running in an app window. A site opened in a browser tab always sees
(application-context: installed)as non-matching, even if the user has the app installed. - No new fingerprinting surface. The information exposed (the current app context) is already inferable from existing signals like
display-mode: standalone, except thatapplication-contextis 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. - Cross-origin iframe isolation. The feature evaluates to
browserin 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 viawindow.topand do not represent a privacy boundary.
- No elevated privileges. The media feature is purely informational; it does not grant any new capabilities.
- No new attack surface. The feature does not interact with system APIs, credentials, or storage in any novel way.
Service 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:
- Stale data. The initial report is a point-in-time snapshot. If the app context changes after the report (a browser tab is captured into an app window, the user uninstalls the app while a page is open, or the user installs and relaunches in a different context), the service worker's map becomes outdated. To stay current, the page must also listen for
matchMediachanges and send follow-up messages, adding further complexity. - Race conditions. The service worker may need to act (e.g., on a
pushevent) 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:
- Lu Huang
- Alison Maher
- Alex Russell
- Rob Paveza
References: