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.
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
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.
5. Deep-link only in the tab form
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.
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.
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_stylewas 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_tabis 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
- Open the options page from the extensions UI and confirm it renders in the form you declared.
- 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/.
- Load
options.html?embedded=1in a tab and confirm nothing overflows horizontally. - 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.
Related
- Opening the options page programmatically — reaching whichever form you declared.
- Building a tabbed options page layout — structure once the tab form is chosen.
- Embedding options in the popup — a third, even smaller surface.
- Options page configuration — the parent guide.