Embedded Versus Full-Page Options Declarations

options_ui with open_in_tab false embeds settings in the browser's extension page; options_page opens a full tab. How each behaves, its size limits, and which to choose in MV3.

Published September 18, 2026 Updated September 18, 2026 7 min read
Table of Contents

The manifest offers two ways to declare an options page and they produce very different surfaces. options_ui with open_in_tab: false renders your page inside the browser’s own extension-details UI, in a constrained dialog-like frame. options_page — or options_ui with open_in_tab: true — opens a normal tab. The embedded form looks more native and is considerably harder to build well; the tab form looks less integrated and is almost impossible to get wrong. This guide is part of options page configuration.

What the user actually sees

Embedded and full-page options comparedThe two declaration forms compared on where they render, available width, scrolling, keyboard behaviour, deep linking and cross-browser consistency.Propertyoptions_ui, embeddedFull tabWhere it rendersInside the extensions UIIts own tabAvailable widthFixed by the browserThe whole windowScrollingNested in the host pageNormal page scrollDeep links (#section)UnreliableWork normallyRoom for a sidebarNoYesLooks nativeYesYour design
Every row where the embedded form is marked partial is something you will have to design around.

Step-by-step

1. Declare the embedded form

1{
2  "options_ui": {
3    "page": "options.html",
4    "open_in_tab": false        // render inside the browser's extension details UI
5  }
6}

Execution context: parsed at install. The page is loaded into a frame the browser controls; you do not choose its width, and the browser’s own chrome surrounds it. It is still a full extension page with the same chrome.* access and CSP as any other.

2. Or declare the full-page form

1{
2  "options_ui": {
3    "page": "options.html",
4    "open_in_tab": true         // a normal tab, full width
5  }
6}

Execution context: parsed at install. options_page (the older key) behaves the same as open_in_tab: true and is still accepted, but options_ui is the form every engine documents, so prefer it for new code.

3. Design the embedded page for its frame

The embedded frame is narrow and auto-sizes to content height. Three habits stop it looking broken:

1/* options.css — embedded-safe defaults */
2html, body { margin: 0; }
3body { min-width: 0; max-width: 100%; padding: 12px 16px; font: 14px/1.5 system-ui, sans-serif; }
4.row { display: grid; grid-template-columns: 1fr auto; gap: 12px; align-items: center; }
5@media (min-width: 720px) { body { padding: 24px 32px; } }   /* full-tab form gets room */

Execution context: the options page’s stylesheet, whichever form it is loaded in. Designing mobile-first means the same page reads well in the narrow embedded frame and spreads out gracefully if you later switch to the tab form.

Avoid position: fixed headers and 100vh layouts in the embedded form — the frame’s viewport is not the window’s, and both produce clipped or doubled scrollbars.

4. Open it programmatically in either form

1// Works for both declarations; the browser picks the right surface.
2document.querySelector("#settings").addEventListener("click", () => {
3  chrome.runtime.openOptionsPage();
4});

Execution context: any extension page or the service worker. openOptionsPage focuses an existing options surface if one is open, rather than opening a second — the behaviour described in opening the options page programmatically.

A link from the popup straight to “Shortcuts” is a good experience, and it only works reliably when the options page is a tab.

1async function openOptionsAt(section) {
2  const url = chrome.runtime.getURL(`options.html#${section}`);
3  const [existing] = await chrome.tabs.query({ url: chrome.runtime.getURL("options.html") + "*" });
4  if (existing) {
5    await chrome.tabs.update(existing.id, { url, active: true });
6  } else {
7    await chrome.tabs.create({ url });
8  }
9}

Execution context: the service worker or an extension page. With the embedded form, openOptionsPage ignores any fragment, so there is no supported way to land the user on a specific section — a meaningful reason to prefer the tab form once the settings outgrow a single screen.

Choosing, and when to switch

The embedded form suits an extension with a handful of settings: a few toggles, a colour, a single list. It feels like part of the browser, which is a genuine quality signal. Once the page needs a sidebar, a search box, import and export, or a table of per-site rules, it has outgrown the frame — and fighting the frame produces a page that looks worse than an honest tab would.

Switching from embedded to tab is a one-line manifest change with no permission impact and no user-visible disruption beyond the surface itself. The page’s code needs only the layout adjustments above, which is why designing mobile-first from the start is worth it.

Which declaration to useA decision tree from the number of settings and the need for navigation or deep links to the embedded or tab declaration.How much does the options page need to hold?A few togglesEmbeddedopen_in_tab: falseFeels nativekeep it to one screenSections and navigationFull tabopen_in_tab: trueDeep links work#section from the popupTables, import, searchFull tabplus a sidebar layoutSee tabbed layoutsfor structure
The switch between them is one manifest key, so choose for today's settings and revisit as they grow.

Testing both forms from one page

Because a later switch is so cheap, it is worth keeping the page working in both forms throughout development rather than discovering the layout assumptions on the day you change the key. A query parameter that simulates the embedded constraints is enough.

1// options.js — ?embedded=1 during development to mimic the frame
2if (new URLSearchParams(location.search).has("embedded")) {
3  document.documentElement.style.maxWidth = "560px";
4  document.documentElement.dataset.embedded = "1";
5}

Execution context: the options page. It does not reproduce the browser’s surrounding chrome, but it does catch the two layout failures that matter — content that overflows a narrow width, and a layout that depends on the window’s full height.

A second check is keyboard reachability. In the embedded form, focus moves between the browser’s own UI and your frame, and a page that traps focus or sets tabindex aggressively makes the browser’s controls unreachable. The standard applied in making popups and options keyboard navigable applies with extra force here.

Usable content width by surfaceApproximate usable width for an options page in the embedded frame, a narrow browser window, a typical laptop window and a full desktop window.Embedded options frame520 pxbrowser-controlledNarrow window, tab form800 pxLaptop window, tab form1280 pxDesktop window, tab form1840 px
The embedded frame is narrower than most phones — design for it and every other width is easy.

Cross-browser variation

  • Chrome / Edge: the embedded form renders in the extension’s details view in chrome://extensions. The frame’s width is fixed by Chrome and has changed across releases, so never design to a pixel value.
  • Firefox: the embedded form renders inside the add-on’s page in about:addons, which is wider than Chrome’s frame and has different surrounding padding. browser_style was historically available to adopt Firefox’s native look; it is deprecated in MV3, so style the page yourself.
  • Safari: options open in a separate window or the Safari settings pane depending on version, and open_in_tab is interpreted loosely. Test the actual surface rather than trusting the manifest value.
  • All three: the page is the same extension page in every form. Storage, messaging and permissions behave identically; only layout and navigation differ.

Verification

  1. Open the options page from the extensions UI and confirm it renders in the form you declared.
  2. Confirm the declaration the browser actually read:
1chrome.runtime.getManifest().options_ui;
2// { page: "options.html", open_in_tab: false }

Execution context: any extension context. A mismatch with your source manifest means the build step rewrote it — check the generated manifest in dist/.

  1. Load options.html?embedded=1 in a tab and confirm nothing overflows horizontally.
  2. Tab through the embedded page with the keyboard and confirm focus can leave it into the browser’s own controls.

FAQ

Can I open the embedded form in a tab when the user wants more room?

Yes — link to chrome.runtime.getURL("options.html") with chrome.tabs.create. The same page loads full-width, which is a reasonable escape hatch for power users.

Does the embedded form share state with a tab-opened copy?

They are two documents of the same extension page, sharing storage. Subscribe to storage.onChanged so a change in one updates the other, as in syncing options form state with chrome.storage.

Why does my options page have two scrollbars?

Almost always a height: 100vh layout in the embedded frame, where the frame and the host page both scroll. Remove the fixed height and let the document size to its content.

Other MV3 Architecture & Extension Lifecycle Resources