Options Page Layouts

Design scalable options page UIs for MV3 extensions — sidebar nav, tabbed sections, responsive grids, form components, autosave UX, and dark mode support.

Without a deliberate layout strategy, options pages devolve into a single scrolling column of unrelated inputs — users cannot find settings, forms submit stale data, and adding a new preference means touching three files. This guide covers the layout patterns that scale: sidebar navigation, tabbed sections, and responsive grids, plus the form and save UX that keeps them coherent across browsers. It is part of the UI/UX Patterns & Interactive Components guide. The manifest registration counterpart — declaring options_ui, controlling open_in_tab, and registering the page — lives in options page configuration.

The constraint that shapes every decision here: options pages run in a standard browser tab context, not a popup. You have the full viewport, full DOM, and no 30-second eviction, but you still cannot block the main thread during storage hydration or attach inline event handlers (CSP blocks them). Every interactive element must be wired via addEventListener in an external script.

Options page layout anatomy — sidebar, tab bar, and content panelA shell div contains a fixed sidebar on the left with nav items, a tab bar across the top of the main area, and a scrollable content panel that renders the active section's form fields.Sidebar NavGeneralPrivacyAppearanceAdvancedNotificationsSyncShortcutsContent Panelform fields · toggle rows · save barhydrated from chrome.storage on mount→ active section

Prerequisites checklist

  • storage permission declared in manifest.json — needed before any read or write in the options page script.
  • options_ui key wired in manifest.json; decide now whether you want open_in_tab: true (full tab, full viewport) or the embedded panel. Details in options page configuration.
  • External JS only — no inline onclick attributes; MV3 CSP rejects them.
  • Versioned storage schema — options page data outlives releases. Design a migration path from day one.
  • Dark-mode CSS — browser-extension UIs default to the browser’s colour scheme, so prefers-color-scheme must be handled explicitly or users get a jarring white flash on dark browsers.

1. Sidebar navigation pattern

A fixed sidebar works best when the options page has four or more distinct sections that users navigate between non-linearly. It leaves the full horizontal width for the content area and makes the active section immediately visible.

The critical constraint: the sidebar must communicate section state through the URL hash, not through JavaScript variables alone. A page reload must restore the active section — if it does not, users who bookmark a deep section or share the URL land on the wrong panel.

 1<!-- options.html — sidebar shell (abridged) -->
 2<div class="opts-shell">
 3  <nav class="opts-sidebar" aria-label="Settings sections">
 4    <ul role="list">
 5      <li><a href="#general" class="opts-navlink" aria-current="page">General</a></li>
 6      <li><a href="#privacy" class="opts-navlink">Privacy</a></li>
 7      <li><a href="#appearance" class="opts-navlink">Appearance</a></li>
 8      <li><a href="#advanced" class="opts-navlink">Advanced</a></li>
 9    </ul>
10  </nav>
11  <main class="opts-content" id="opts-main">
12    <section id="general" class="opts-panel" aria-labelledby="general-heading">
13      <h2 id="general-heading">General</h2>
14      <!-- form fields -->
15    </section>
16    <section id="privacy" class="opts-panel" hidden aria-labelledby="privacy-heading">
17      <h2 id="privacy-heading">Privacy</h2>
18    </section>
19  </main>
20</div>

Execution context: Renders in a standard browser tab (or the embedded options panel if open_in_tab is omitted). Inline scripts are blocked by MV3 CSP — attach all nav behaviour in options.js loaded via <script src="options.js" type="module">.

 1// options.js — hash-driven sidebar activation
 2function activateSection(hash) {
 3  const id = hash.replace('#', '') || 'general';
 4  document.querySelectorAll('.opts-panel').forEach(panel => {
 5    panel.hidden = panel.id !== id;
 6  });
 7  document.querySelectorAll('.opts-navlink').forEach(link => {
 8    const active = link.getAttribute('href') === `#${id}`;
 9    link.setAttribute('aria-current', active ? 'page' : 'false');
10  });
11}
12
13window.addEventListener('hashchange', () => activateSection(location.hash));
14activateSection(location.hash); // restore on load / reload

Execution context: Runs in the options page renderer process (the same tab). location.hash is available immediately; no extension API needed for routing. Chrome, Firefox, and Safari all fire hashchange on the options tab.

2. Tabbed section pattern

Tabs make sense when one section dominates usage and sub-topics are closely related — a “Notifications” panel with tabs for Email, Push, and In-App alerts, for example. The full-width tab bar announces the sub-topics upfront without requiring sidebar space.

Do not use <div> tabs. The building a tabbed options page layout guide walks through the complete ARIA tab pattern, keyboard navigation, and deep-linking via hash. The minimal wiring looks like this:

 1<!-- Tab bar -->
 2<div role="tablist" aria-label="Notification settings" class="opts-tablist">
 3  <button role="tab" aria-selected="true" aria-controls="panel-email" id="tab-email">
 4    Email
 5  </button>
 6  <button role="tab" aria-selected="false" aria-controls="panel-push" id="tab-push" tabindex="-1">
 7    Push
 8  </button>
 9</div>
10<div role="tabpanel" id="panel-email" aria-labelledby="tab-email">
11  <!-- email notification fields -->
12</div>
13<div role="tabpanel" id="panel-push" aria-labelledby="tab-push" hidden>
14  <!-- push fields -->
15</div>

Execution context: Options page renderer. role="tablist" + role="tab" + role="tabpanel" is the only ARIA pattern that screen readers announce as a tab component. Chrome, Firefox, and Safari all support it; test with NVDA and VoiceOver.

3. Responsive grid for settings rows

Individual setting rows — a label, a control, and optional helper text — benefit from a two-column grid that aligns labels on the left with controls on the right. Below 600 px the grid collapses to a single column so the options page stays usable on narrow viewport extensions.

One settings row at three widthsA two-column grid of label and control collapses to a stacked single column below the narrow breakpoint used by extension surfaces.Wide tablabel | control | helpEmbedded panellabel above controlSide-by-side popupcontrol only, label as ariaone grid definition, two breakpointsgrid-template-columnsminmax(0, 18rem) 1frCollapse below 32remsingle column
An options page can be opened as a full tab or as a narrow embedded panel — the same row has to work at both.
 1/* options.css */
 2.opts-shell {
 3  display: grid;
 4  grid-template-columns: 200px 1fr;
 5  min-height: 100vh;
 6  color-scheme: light dark; /* honour OS preference for scrollbars and system colours */
 7}
 8
 9.opts-sidebar {
10  position: sticky;
11  top: 0;
12  height: 100vh;
13  overflow-y: auto;
14  padding: 1.5rem 1rem;
15  border-right: 1px solid color-mix(in srgb, currentColor 12%, transparent);
16}
17
18.opts-content {
19  padding: 2rem;
20  max-width: 720px;
21}
22
23.opts-row {
24  display: grid;
25  grid-template-columns: 1fr auto;
26  align-items: center;
27  gap: 1rem;
28  padding: 0.75rem 0;
29  border-bottom: 1px solid color-mix(in srgb, currentColor 8%, transparent);
30}
31
32@media (max-width: 600px) {
33  .opts-shell {
34    grid-template-columns: 1fr;
35  }
36  .opts-sidebar {
37    position: static;
38    height: auto;
39    border-right: none;
40    border-bottom: 1px solid color-mix(in srgb, currentColor 12%, transparent);
41  }
42  .opts-row {
43    grid-template-columns: 1fr;
44  }
45}

Execution context: CSS loaded from a file in the extension package (<link rel="stylesheet" href="options.css">). Remote stylesheets are blocked by MV3 CSP unless you add an explicit content_security_policy exception — avoid them. color-mix() is supported in Chrome 111+, Firefox 113+, and Safari 16.2+; add a fallback border-color if you target earlier versions.

4. Form components and save UX

Options forms have two failure modes. The first is saving on every keystroke and hitting the chrome.storage.sync write-rate limit (~120 writes/minute). The second is requiring an explicit Save button and then silently discarding changes if the user closes the tab. The right pattern is debounced autosave with visible feedback.

The full two-way binding implementation — reading on mount, writing on change, reacting to onChanged in other tabs — is covered in syncing options form state with chrome.storage. The save-bar pattern shown here gives users a clear confirmation without a blocking modal:

 1// save-bar.js — autosave with status indicator
 2const statusEl = document.getElementById('opts-save-status');
 3
 4function showStatus(msg, isError = false) {
 5  statusEl.textContent = msg;
 6  statusEl.className = isError ? 'opts-status opts-status--error' : 'opts-status opts-status--ok';
 7  statusEl.hidden = false;
 8  setTimeout(() => { statusEl.hidden = true; }, 2500);
 9}
10
11let saveTimer = null;
12
13export function scheduleSave(key, value) {
14  clearTimeout(saveTimer);
15  saveTimer = setTimeout(async () => {
16    try {
17      await chrome.storage.sync.set({ [key]: value });
18      showStatus('Saved');
19    } catch (err) {
20      showStatus('Save failed — storage quota exceeded', true);
21    }
22  }, 400); // 400 ms debounce keeps writes well under the rate limit
23}

Execution context: Options page renderer. chrome.storage.sync.set returns a Promise in MV3 — no callback form needed. The 400 ms debounce is deliberately longer than the 300 ms UI interaction debounce to stay safely below the per-minute write cap. Firefox and Safari honour the same chrome.storage.sync surface (via the WebExtensions polyfill for Firefox or native on Safari 15.4+).

5. Dark mode support

Extension pages inherit the browser’s preferred colour scheme but do not automatically apply it to custom styles. Failing to handle dark mode means a glaring white options page inside a dark-themed browser — the most common visual complaint in extension reviews.

How a theme choice reaches every extension surfaceThe OS preference seeds the default, the user's stored override wins, and each surface applies it through the same custom-property layer.prefers-color-schemethe default on first runnever overwrite it silentlyStored overridechrome.storage.syncfollows the user across devicesdata-theme on the rootset before first paintavoids a flashCSS custom propertiesone token set per themepopup, options and panel share it
Store the choice, not the resolved colour — a stored colour cannot follow the user when they switch their system theme.
 1/* Prefer @media over JS for zero-flash dark support */
 2:root {
 3  --opts-bg: #ffffff;
 4  --opts-surface: #f5f5f5;
 5  --opts-text: #111111;
 6  --opts-accent: #2563eb;
 7  --opts-border: rgba(0, 0, 0, 0.10);
 8}
 9
10@media (prefers-color-scheme: dark) {
11  :root {
12    --opts-bg: #1a1a1a;
13    --opts-surface: #242424;
14    --opts-text: #ededed;
15    --opts-accent: #60a5fa;
16    --opts-border: rgba(255, 255, 255, 0.10);
17  }
18}
19
20body {
21  background: var(--opts-bg);
22  color: var(--opts-text);
23}
24
25/* User-selectable theme override stored in chrome.storage.sync */
26[data-theme="dark"] {
27  --opts-bg: #1a1a1a;
28  --opts-surface: #242424;
29  --opts-text: #ededed;
30  --opts-accent: #60a5fa;
31  --opts-border: rgba(255, 255, 255, 0.10);
32}
33
34[data-theme="light"] {
35  --opts-bg: #ffffff;
36  --opts-surface: #f5f5f5;
37  --opts-text: #111111;
38  --opts-accent: #2563eb;
39  --opts-border: rgba(0, 0, 0, 0.10);
40}

Execution context: Static CSS file loaded in options.html. Apply the data-theme attribute to <html> immediately after reading from chrome.storage.sync — before DOMContentLoaded if possible — to avoid a flash of wrong theme. Chrome, Firefox, and Safari all respect prefers-color-scheme; the color-scheme property on :root additionally adjusts system UI elements like scrollbars and input borders.

6. Organising settings around tasks, not implementation

The most common structural problem in extension options pages is that they mirror the code rather than the user. Settings appear in the order they were added, grouped by the module that reads them, labelled with the names developers use internally. The result is a page that makes sense to the person who wrote it and to nobody else.

A better organisation starts from what users come to the options page to do. For most extensions that is a short list: change how the extension looks, choose where it runs, control what it does automatically, manage their account or data, and get help. Each of those becomes a section, and each setting goes into the section that matches the task a user would be thinking of when they want to change it — not the module it happens to live in.

Within a section, order settings by how often they are changed. The one setting most users adjust goes first, visible without scrolling; rarely used options go further down or behind an “Advanced” disclosure. Labels describe the outcome rather than the mechanism: “Save pages automatically after a minute of reading” says what happens, where “Auto-capture threshold” says how it is implemented.

7. Settings that affect other surfaces

Many settings change something the user cannot see from the options page: the popup’s layout, the badge, what happens on a site. When the effect is invisible, users are left unsure whether a change worked. Close the loop wherever possible. A small live preview beside visual settings, a sentence describing the current behaviour beside behavioural ones, and a confirmation that names the effect — “Badge now shows unread count” rather than “Saved” — all help. Where the change only takes effect on the next page load, say so, and offer to reload the current tab.

These settings must also propagate. The options page writes to storage; every other surface — the popup, side panel, content scripts and the service worker — should react through storage.onChanged rather than reading once at startup, so a change applies everywhere immediately, as described in syncing options form state with chrome.storage.

8. Destructive and advanced actions

Some options page actions cannot be undone: clearing saved data, resetting all settings, revoking access, signing out everywhere. They belong at the bottom of the page, visually separated, with plain language about the consequence and a confirmation step that names exactly what will be lost. A native <dialog> element works well for this in extension pages and is accessible by default.

Where it is cheap, make destructive actions reversible instead. Keeping a local backup of settings before a reset, or moving deleted items to a recoverable state for a few days, turns an irreversible action into a safe one and removes the need for alarming confirmation copy. The validation and reset patterns are covered in validating and resetting options forms.

9. Growing the page without losing users

Options pages grow with the product. What starts as five toggles becomes fifty settings across several sections, and the layout that worked at five stops working. Plan for the growth in stages. At a dozen settings, add section headings and a table of contents. At a few dozen, add search that matches labels, help text and synonyms, as described in search and filter in a large options page. Beyond that, consider whether some settings belong elsewhere — per-site options in the popup, where the user already is when they want to change them, and power-user options behind an explicit advanced mode. The aim throughout is that the setting a user wants is never more than a glance or a search away, however large the page becomes.

10. Testing the page with real settings

An options page is usually designed and tested with default values, which is the one state most long-term users are never in. Test it with a realistic, heavily customised profile instead: dozens of enabled sites, long custom labels, every optional feature switched on, a language with long words, and a narrow window. Those conditions expose overflowing rows, lists that need scrolling within scrolling, labels that wrap under their controls, and save indicators that appear far from the setting they refer to. A fixture that seeds such a profile into storage makes the check repeatable, and running it at both the embedded options width and a full tab width covers the two ways the page is actually displayed.

Repeat the check with a screen reader and with the keyboard alone, because a crowded options page is also where focus order most often drifts away from the visual order. Fixing both at the same time, against the same realistic profile, is far cheaper than discovering each problem separately from user reports after release.

MV3 constraints to design around

  • No inline scripts: MV3 CSP (script-src 'self') rejects onclick, onchange, and <script> tags with inline content. All listeners attach via addEventListener in external .js files.
  • No remote resources: remote stylesheets and scripts are blocked. Bundle everything in the extension package.
  • Storage quota: chrome.storage.sync caps at 8 KB per item and ~120 writes/minute. Debounce all form writes.
  • No localStorage for cross-context state: localStorage is scoped to the extension’s origin but is not observable from the service worker. Use chrome.storage for any state that the service worker or content scripts also need.
  • Options page is not always open: never assume the options page is listening. The service worker must use chrome.storage.onChanged or message passing, not a direct DOM call.

Cross-browser notes

Chrome and Edge: full support for open_in_tab, chrome.storage.sync, and chrome.runtime.openOptionsPage(). Automatic conflict resolution for sync writes across signed-in profiles.

Firefox: browser.runtime.openOptionsPage() is the native call; chrome.runtime.openOptionsPage() works only with the webextension-polyfill. Firefox 109+ ships MV3 but still ships some MV2 APIs in parallel. The embedded panel (without open_in_tab) is poorly supported in Firefox — prefer open_in_tab: true for widest compatibility.

Safari: options pages work in Safari 15.4+ with WebKit’s MV3 implementation. prefers-color-scheme works, but color-mix() requires Safari 16.2+. Safari’s sync backend maps to iCloud, with stricter per-extension quotas and potential throttling on cellular connections. Test save flows under network constraints.

Further guides in this topic

The guides below go deeper into specific options page layouts problems that the sections above only touch on — each one starts from a concrete symptom and ends with a way to verify the fix.

  • Accessible Form Controls for Extension Settings — Build extension settings that work with a keyboard and a screen reader — labelled toggles, grouped radios, custom switches done properly, inline validation and save feedback that is announced.
  • Search and Filter in a Large Options Page — Add search to an extension options page with dozens of settings — an index built from the DOM, highlighting matches, keeping sections navigable, and announcing results accessibly.
  • Auto-Save vs Explicit Save in Options — Decide between auto-saving extension settings and a Save button: how each feels, chrome.storage.sync write limits, saved-state feedback, validation, unsaved-changes warnings, and a hybrid that suits most extensions.
  • Building an Options Page with React — Build an MV3 extension options page in React: a typed settings hook backed by chrome.storage, live updates from other contexts, forms with validation, embedding in chrome://extensions, CSP-compatible builds, and testing.
  • Embedding Options in the Popup — Show settings inside an MV3 popup without duplicating the options page: a shared form module, the narrow-surface layout, which settings to omit, and linking out for the rest.
  • Showing Permission Status on the Options Page — Show users which permissions and site access an MV3 extension has: listing granted and optional permissions with chrome.permissions, requesting and removing them from options, explaining host access, and reacting to changes made elsewhere.
  • Theming the Options Page for Dark Mode — Give an extension’s options page light, dark and system themes: CSS custom properties, prefers-color-scheme, color-scheme for native controls, a stored theme override applied before first paint, the embedded options frame, and sharing tokens with the popup.

Auto-Save vs Explicit Save in Options

Decide between auto-saving extension settings and a Save button: how each feels, chrome.storage.sync write limits, saved-state feedback, validation, unsaved-changes warnings, and a hybrid that suits most extensions.

Building an Options Page with React

Build an MV3 extension options page in React: a typed settings hook backed by chrome.storage, live updates from other contexts, forms with validation, embedding in chrome://extensions, CSP-compatible builds, and testing.

Showing Permission Status on the Options Page

Show users which permissions and site access an MV3 extension has: listing granted and optional permissions with chrome.permissions, requesting and removing them from options, explaining host access, and reacting to changes made elsewhere.

Theming the Options Page for Dark Mode

Give an extension's options page light, dark and system themes: CSS custom properties, prefers-color-scheme, color-scheme for native controls, a stored theme override applied before first paint, the embedded options frame, and sharing tokens with the popup.

Accessible Form Controls for Extension Settings

Build extension settings that work with a keyboard and a screen reader — labelled toggles, grouped radios, custom switches done properly, inline validation and save feedback that is announced.

Search and Filter in a Large Options Page

Add search to an extension options page with dozens of settings — an index built from the DOM, highlighting matches, keeping sections navigable, and announcing results accessibly.

Embedding Options in the Popup

Show settings inside an MV3 popup without duplicating the options page: a shared form module, the narrow-surface layout, which settings to omit, and linking out for the rest.

Validating and Resetting Options Forms

Validate an MV3 options form without inline handlers: constraint validation plus semantic checks, accessible error messaging, safe reset to defaults, and import and export of settings.

Building a Tabbed Options Page Layout

Implement accessible ARIA tabs in an MV3 options page — keyboard navigation, deep-linking via URL hash, plain JS and React versions, and cross-browser testing.

Syncing Options Form State with chrome.storage

Two-way bind an MV3 options form to chrome.storage.sync — debounced autosave, onChanged cross-tab reactivity, optimistic UI, and quota error handling.