# PWAs on Meta Quest (Horizon OS browser)

> How web apps for Meta Horizon OS use Bubblewrap and Trusted Web Activity, with WebXR detection and a non-immersive fallback.

Meta Horizon OS supports web-based 2D experiences that can be distributed through the Meta Horizon Store. Meta documents Bubblewrap as the tool that packages a PWA into an Android App Bundle and launches its web content through a Trusted Web Activity ([Start Building with Meta Quest 3S](https://developers.meta.com/vr/blog/start-building-meta-quest-3s-3-launch-spatial-sdk-2D-mixed-reality/), Meta Horizon OS Developers).

## Platform support

Meta documents Meta Horizon Store distribution for web-based 2D experiences on Meta Horizon OS. For immersive features, MDN marks `XRSystem.isSessionSupported()` as limited availability and available only in secure contexts in supporting browsers; the method checks whether the user's WebXR device supports a requested session mode ([Start Building with Meta Quest 3S](https://developers.meta.com/vr/blog/start-building-meta-quest-3s-3-launch-spatial-sdk-2D-mixed-reality/), Meta Horizon OS Developers; [XRSystem: isSessionSupported() method](https://developer.mozilla.org/en-US/docs/Web/API/XRSystem/isSessionSupported), MDN).

## How it works

A Trusted Web Activity opens PWA content from an Android app through a protocol based on Custom Tabs. Digital Asset Links verify that the Android app and the site come from the same developer, while the user's browser renders the web content ([Trusted Web Activity overview](https://developer.chrome.com/docs/android/trusted-web-activity/), Chrome for Developers).

:::caution
`isSessionSupported()` rejects its promise with a `SecurityError` when the `xr-spatial-tracking` Permissions Policy blocks WebXR. Handle rejection as well as a `false` result ([XRSystem: isSessionSupported() method](https://developer.mozilla.org/en-US/docs/Web/API/XRSystem/isSessionSupported), MDN).
:::

## Examples

The example keeps ordinary page content available unless the browser reports support for an immersive VR session.

### Reveal an immersive control after checking support

Paste this complete page into an HTTPS origin. It checks for `navigator.xr`, handles both the unsupported and rejected paths, and leaves the non-immersive content visible.

```html
<!doctype html>
<html lang="en">
  <body>
    <p>This content works without an immersive session.</p>
    <button id="enter-xr" hidden>Enter XR</button>
    <output id="xr-status">Checking WebXR support...</output>
    <script>
      const button = document.querySelector('#enter-xr');
      const status = document.querySelector('#xr-status');

      if (!('xr' in navigator)) {
        status.value = 'Continue without immersive mode.';
      } else {
        navigator.xr.isSessionSupported('immersive-vr').then((supported) => {
          button.hidden = !supported;
          status.value = supported
            ? 'Immersive mode is available.'
            : 'Continue without immersive mode.';
        }).catch(() => {
          button.hidden = true;
          status.value = 'Continue without immersive mode.';
        });
      }
    </script>
  </body>
</html>
```

The button remains hidden when `navigator.xr` is absent, the requested session mode is unsupported, or the support check rejects.

### Read launch data from the URL

A Trusted Web Activity host can pass data to its web content in a URL, including through query parameters ([Trusted Web Activity overview](https://developer.chrome.com/docs/android/trusted-web-activity/), Chrome for Developers). Save this page as `launch.html`, then open `launch.html?source=quest`.

```html
<!doctype html>
<html lang="en">
  <body>
    <output id="launch-source">No launch source supplied.</output>
    <script>
      const source = new URLSearchParams(location.search).get('source');
      if (source) {
        document.querySelector('#launch-source').value = `Launched from ${source}.`;
      }
    </script>
  </body>
</html>
```

The output reads `Launched from quest.` when the query parameter is present and retains the fallback text when it is absent.

## See also

- [Manifest scope](/reference/manifest/scope/)
- [Installable PWAs](/guides/installable/)
- [Start Building with Meta Quest 3S](https://developers.meta.com/vr/blog/start-building-meta-quest-3s-3-launch-spatial-sdk-2D-mixed-reality/) (Meta Horizon OS Developers)
- [Trusted Web Activity overview](https://developer.chrome.com/docs/android/trusted-web-activity/) (Chrome for Developers)
- [XRSystem: isSessionSupported() method](https://developer.mozilla.org/en-US/docs/Web/API/XRSystem/isSessionSupported) (MDN)