MSEdgeExplainers

Application Context Media Feature

Authors

Participate

Status of this Document

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.

Table of Contents

Introduction

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:

The display-mode Problem

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.

Goals

Non-goals

Proposed Solution

Syntax

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.

Behavior Rules

  1. Matches 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.
  2. Remains 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.
  3. 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 via window.top.
  4. Matches 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.
  5. Usable via matchMedia(). JavaScript can query and listen for changes using window.matchMedia(), following standard media query semantics.

Behavior Summary

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.

Key Scenarios

Scenario 1: Hiding the Install Prompt

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.

Scenario 2: App-Specific Navigation

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;
  }
}

Scenario 3: JavaScript Detection

A site conditionally shows a service worker update prompt only in the installed experience:

if (window.matchMedia('(application-context: installed)').matches) {
  showUpdatePrompt();
}

Scenario 4: Listening for Context Changes

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);
});

Alternatives Considered

A Boolean Media Feature (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:

Conclusion: While the boolean form is simpler for a binary state check, the application-context enumerated approach offers clearer semantics and room to grow.

Standardizing navigator.standalone

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:

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.

Extending display-mode with a New Value

One option is adding a value like display-mode: installed. However:

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 New JavaScript API (navigator.isInstalled)

A dedicated JS property could work, but:

Conclusion: The CSS media feature, accessible via matchMedia(), covers both CSS and JS use cases with a single mechanism.

Privacy and Security Considerations

Privacy

Security

Future Extension: Service Worker clients Integration

Background: Service Workers and the Clients API

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.

Current Workaround: Message-Passing

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:

Proposed Extension

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.

References & Acknowledgements

Many thanks for valuable feedback and advice from:

References: