Chrome Extension Options Page Patterns — Best Practices
9 min readOptions 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:
open_in_tab: false(default): Inline embedded options within chrome://extensionsopen_in_tab: true: Opens as a full tab like legacyoptions_page
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
- Use
<fieldset>and<legend>for grouped controls - Ensure all inputs have associated
<label>elements - Use
aria-describedbyfor error messages - Support Tab navigation and Enter/Space activation
- Add
aria-liveregions for status messages
<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();
Related Resources
Part of the Chrome Extension Guide by theluckystrike. Built at zovo.one.