Chrome Extension Options Page Patterns — Best Practices

9 min read

Options Page Design Patterns

This document covers design and implementation patterns for Chrome extension options pages.

Manifest Options: options_page vs options_ui

Chrome provides two manifest approaches for options pages:

Legacy: options_page

{
  "options_page": "options.html"
}

Creates a full browser tab for settings. Simple but opens outside the extensions management UI.

Modern: options_ui (Preferred)

{
  "options_ui": {
    "page": "options.html",
    "open_in_tab": false
  }
}

Embeds options in chrome://extensions page. The open_in_tab key controls behavior:

Recommendation: Use options_ui with default settings for better integration.

Settings Form Patterns

Auto-Save on Change

document.querySelectorAll('input, select, textarea').forEach(el => {
  el.addEventListener('change', async () => {
    const settings = collectFormValues();
    await chrome.storage.sync.set(settings);
    showSaveIndicator();
  });
});

function collectFormValues() {
  return {
    theme: document.getElementById('theme').value,
    notifications: document.getElementById('notifications').checked,
    refreshInterval: parseInt(document.getElementById('interval').value, 10)
  };
}

Explicit Save Button

document.getElementById('save-btn').addEventListener('click', async () => {
  const settings = collectFormValues();
  if (validateSettings(settings)) {
    await chrome.storage.sync.set(settings);
    showSuccessMessage('Settings saved!');
  }
});

Loading State

Load saved settings on page initialization:

document.addEventListener('DOMContentLoaded', async () => {
  const settings = await chrome.storage.sync.get(null);
  
  document.getElementById('theme').value = settings.theme || 'light';
  document.getElementById('notifications').checked = settings.notifications ?? true;
  document.getElementById('interval').value = settings.refreshInterval || 30;
});

Validation

Client-side validation before saving:

function validateSettings(settings) {
  const errors = [];
  
  if (settings.refreshInterval < 5 || settings.refreshInterval > 3600) {
    errors.push('Refresh interval must be between 5 and 3600 seconds');
  }
  
  if (settings.apiKey && !settings.apiKey.startsWith('pk_')) {
    errors.push('API key must start with pk_');
  }
  
  if (errors.length > 0) {
    showErrors(errors);
    return false;
  }
  return true;
}

Settings Organization

Tabbed Settings Page

<div class="tabs">
  <button class="tab active" data-tab="general">General</button>
  <button class="tab" data-tab="appearance">Appearance</button>
  <button class="tab" data-tab="advanced">Advanced</button>
</div>

<div id="general" class="tab-content active">
  <!-- General settings -->
</div>
<div id="appearance" class="tab-content">
  <!-- Appearance settings -->
</div>
<div id="advanced" class="tab-content">
  <!-- Advanced settings -->
</div>

Section-Based Layout

For simpler settings, use collapsible sections:

<details>
  <summary>Notifications</summary>
  <div class="settings-group">
    <label><input type="checkbox" id="notify-updates">Notify of updates</label>
    <label><input type="checkbox" id="notify-errors">Notify of errors</label>
  </div>
</details>

Reset to Defaults

document.getElementById('reset-btn').addEventListener('click', async () => {
  if (confirm('Reset all settings to defaults?')) {
    const defaults = {
      theme: 'light',
      notifications: true,
      refreshInterval: 30
    };
    await chrome.storage.sync.set(defaults);
    loadSettings(defaults);
    showSuccessMessage('Settings reset to defaults');
  }
});

Import/Export Settings

Export to JSON

document.getElementById('export-btn').addEventListener('click', async () => {
  const settings = await chrome.storage.sync.get(null);
  const blob = new Blob([JSON.stringify(settings, null, 2)], { type: 'application/json' });
  const url = URL.createObjectURL(blob);
  
  const a = document.createElement('a');
  a.href = url;
  a.download = 'extension-settings.json';
  a.click();
  URL.revokeObjectURL(url);
});

Import from JSON

document.getElementById('import-btn').addEventListener('change', async (e) => {
  const file = e.target.files[0];
  const reader = new FileReader();
  
  reader.onload = async (event) => {
    try {
      const settings = JSON.parse(event.target.result);
      if (validateSettings(settings)) {
        await chrome.storage.sync.set(settings);
        loadSettings(settings);
        showSuccessMessage('Settings imported successfully');
      }
    } catch (err) {
      showErrors(['Invalid settings file']);
    }
  };
  reader.readAsText(file);
});

Settings Sync

Use chrome.storage.sync for cross-device synchronization:

// Settings automatically sync across user's devices
await chrome.storage.sync.set({ theme: 'dark' });

// Listen for changes from other devices
chrome.storage.onChanged.addListener((changes, area) => {
  if (area === 'sync') {
    applyTheme(changes.theme.newValue);
  }
});

Dynamic Settings

Show/hide options based on other settings:

document.getElementById('theme').addEventListener('change', (e) => {
  const customColorSection = document.getElementById('custom-color-section');
  customColorSection.style.display = e.target.value === 'custom' ? 'block' : 'none';
});

Number Inputs: Range Sliders

<label for="opacity">Opacity: <span id="opacity-value">80</span>%</label>
<input type="range" id="opacity" min="0" max="100" value="80">
document.getElementById('opacity').addEventListener('input', (e) => {
  document.getElementById('opacity-value').textContent = e.target.value;
});

Color Pickers

<input type="color" id="accent-color" value="#3498db">
document.getElementById('accent-color').addEventListener('change', async (e) => {
  await chrome.storage.sync.set({ accentColor: e.target.value });
});

Keyboard Navigation and Accessibility

<fieldset>
  <legend>Display Settings</legend>
  <label for="theme">Theme</label>
  <select id="theme" aria-describedby="theme-desc">
    <option value="light">Light</option>
    <option value="dark">Dark</option>
  </select>
  <small id="theme-desc">Choose your preferred color scheme</small>
</fieldset>

Settings Migration

Handle schema changes between versions:

async function migrateSettings() {
  const version = (await chrome.storage.sync.get('settingsVersion')).settingsVersion || 1;
  
  if (version < 2) {
    // Migrate from v1 to v2
    const oldSettings = await chrome.storage.sync.get(['oldSetting']);
    if (oldSettings.oldSetting) {
      await chrome.storage.sync.set({ newSetting: oldSettings.oldSetting });
      await chrome.storage.sync.remove('oldSetting');
    }
  }
  
  await chrome.storage.sync.set({ settingsVersion: 2 });
}

// Run on options page load
migrateSettings();

Part of the Chrome Extension Guide by theluckystrike. Built at zovo.one.