# tabbed display mode and tab_strip manifest member

> The tabbed display_override value gives an installed PWA its own tab strip, and the tab_strip member configures its pinned home tab and new-tab button.

`tabbed` is a display mode that gives an installed app a tab strip inside its own window, so
the user can keep several in-scope pages open side by side without the browser's address bar.
It is requested through `display_override`, not through `display`, and the companion
`tab_strip` member configures the strip: a pinned home tab with its own URL scope and the URL
the new-tab button opens.

Chrome 126, Edge 126, and Samsung Internet 28.0 process the value (BCD
`html.manifest.display_override.tabbed`); the Chrome for Developers page records the feature as
shipped on ChromeOS, and desktop Chrome on Windows, macOS, and Linux shows the strip for
installed apps from the same milestone. Firefox 157, Safari 27, and Android WebView skip the
entry and use the next `display_override` value or `display`.

## Member

- **Type**: `"tabbed"` is a string inside the `display_override` array. `tab_strip` is an
  object with two optional keys: `home_tab` (`false`, or an object with `scope_patterns`, an
  array of URL pattern objects resolved against the manifest URL, and `icons`) and
  `new_tab_button` (an object with `url`, resolved against the manifest URL and required to be
  within `scope`).
- **Default**: without `tab_strip` the app behaves as `{ "tab_strip": { "new_tab_button": { "url": "<start_url>" } } }`
  and has no home tab. `tab_strip` is ignored unless the resolved display mode is `tabbed`.
- **Example value**: `"display_override": ["tabbed"]` with
  `"tab_strip": { "home_tab": { "scope_patterns": [{ "pathname": "/dashboard/*" }] }, "new_tab_button": { "url": "/new" } }`.

The home tab is pinned: navigations to a URL matching one of its `scope_patterns` stay in it,
and navigations outside those patterns open a new app tab. When `home_tab` is present but
`scope_patterns` is absent, the home tab's scope collapses to `start_url` alone. The new-tab
button is only rendered when its `url` falls outside the home tab's scope; a `url` inside it
hides the button, because the home tab already covers that destination (WICG explainer).

:::observed
Chrome 155 on macOS 26 (English UI), installed app with `"display_override": ["tabbed"]`:
`matchMedia('(display-mode: tabbed)').matches` returns `true` in the app window and `false`
when the same origin is opened in a browser tab, and the app window draws a tab strip with a
**+** button at the position the `new_tab_button.url` would open. The same manifest opened in
Chrome 125 returns `false` for the query and launches in the `standalone` mode named by
`display`.
:::

## Examples

The three manifests describe the same project-management app with a dashboard at
`/dashboard/` and per-project pages at `/projects/<id>/`.

### Opting in with a standalone fallback

`tabbed` goes first in `display_override`; `display` stays `standalone` so Chrome 125,
Firefox, and Safari launch an ordinary app window. Nothing else is required for a working tab
strip, because the default `tab_strip` opens new tabs at `start_url`.

```json
{
  "start_url": "/dashboard/",
  "scope": "/",
  "display": "standalone",
  "display_override": ["tabbed"]
}
```

With this manifest every link to a project page opens in a new app tab, and the dashboard
is just one tab among them, closable like any other.

### Pinning the dashboard as the home tab

Adding `home_tab` turns the dashboard into a pinned first tab that stays open and that
absorbs every navigation under `/dashboard/`. The new-tab button points at `/projects/new`,
which is outside the home tab's patterns, so the button is shown.

```json
{
  "start_url": "/dashboard/",
  "scope": "/",
  "display": "standalone",
  "display_override": ["tabbed"],
  "tab_strip": {
    "home_tab": {
      "scope_patterns": [{ "pathname": "/dashboard/*" }],
      "icons": [{ "src": "/icons/home-96.png", "sizes": "96x96" }]
    },
    "new_tab_button": { "url": "/projects/new" }
  }
}
```

Pointing `new_tab_button.url` at `/dashboard/` instead would hide the button, since that
destination is already the home tab.

### Adapting navigation to the mode the browser chose

A tabbed app can let the OS tab strip replace an in-page tab bar, but only when the mode was
granted. The check runs at startup and again on change, because the user can move the window
between modes.

```js
const tabbed = matchMedia('(display-mode: tabbed)');

function applyMode(active) {
  document.documentElement.dataset.nav = active ? 'os-tabs' : 'in-page';
}

applyMode(tabbed.matches);
tabbed.addEventListener('change', (event) => applyMode(event.matches));
```

With `data-nav="in-page"` the app renders its own tab bar; with `os-tabs` it hides that bar
and opens project pages with `window.open(url)` so each becomes an app tab.

## See also

- [display_override manifest member](/reference/manifest/display-override/)
- [display manifest member](/reference/manifest/display/)
- [Desktop PWA installation: Chrome, Edge, and beyond](/reference/installation/desktop-install/)
- [Manifest Incubations: tabbed application mode explainer](https://wicg.github.io/manifest-incubations/tabbed-mode-explainer.html) (wicg.github.io)
- [Tabbed application mode for PWAs](https://developer.chrome.com/docs/capabilities/tabbed-application-mode) (developer.chrome.com)