Build a Command Palette Chrome Extension: Complete 2025 Guide
Command palette extensions have revolutionized how users interact with web applications and browsers. Inspired by macOS Spotlight and similar tools, these interfaces provide a quick, keyboard-driven way to access features, search content, and navigate applications. In this comprehensive guide, we will walk you through building a fully functional command palette Chrome extension that you can customize and extend for your own projects.
Whether you want to create a command bar Chrome extension for personal use or develop a spotlight search extension for distribution, this guide covers everything from the basic architecture to advanced features and best practices.
What is a Command Palette Extension?
A command palette is a modal overlay that appears when triggered, typically via a keyboard shortcut like Ctrl+K or Cmd+K. It provides a text input field where users can type commands, search through actions, or find content within the application. The interface then filters and displays matching results in real-time, allowing for instant execution of selected actions.
The popularity of command palettes exploded after VS Code adopted them, and Chrome extensions have followed suit. A well-implemented command palette extension can dramatically improve user productivity by reducing the need for mouse navigation and providing quick access to hidden features.
Why Build a Command Palette Chrome Extension?
There are several compelling reasons to build a command bar Chrome extension:
-
Improved Productivity: Users can perform complex actions with a few keystrokes, eliminating the need to navigate through multiple menus.
-
Enhanced Discoverability: Command palettes make it easy to discover and access features that might otherwise be hidden in nested menus.
-
Keyboard-Centric Workflow: Power users prefer keyboard-driven interfaces, and command palettes cater to this preference perfectly.
-
Cross-Application Access: As a Chrome extension, your command palette can interact with web pages, browser features, and external APIs.
-
Customization: Users can add their own commands, creating personalized workflows tailored to their specific needs.
Project Setup and Architecture
Let’s start building our command palette extension. We’ll use Manifest V3, the latest Chrome extension manifest version.
Directory Structure
Create the following directory structure for your project:
command-palette-extension/
├── manifest.json
├── popup.html
├── popup.js
├── styles.css
├── background.js
├── content.js
└── icons/
├── icon16.png
├── icon48.png
└── icon128.png
Manifest Configuration
The manifest.json file defines the extension’s configuration and permissions:
{
"manifest_version": 3,
"name": "Command Palette",
"version": "1.0.0",
"description": "A powerful command palette for Chrome",
"permissions": [
"storage",
"activeTab",
"scripting"
],
"action": {
"default_popup": "popup.html",
"default_icon": {
"16": "icons/icon16.png",
"48": "icons/icon48.png",
"128": "icons/icon128.png"
}
},
"background": {
"service_worker": "background.js"
},
"commands": {
"toggle-command-palette": {
"suggested_key": {
"default": "Ctrl+Shift+P",
"mac": "Command+Shift+P"
},
"description": "Toggle Command Palette"
}
}
}
This configuration sets up the extension with keyboard shortcuts, a popup interface, and the necessary permissions for functionality.
Building the Command Palette Interface
The core of any command palette extension is its user interface. Let’s create a responsive, feature-rich interface that feels native to Chrome.
HTML Structure
The popup.html file contains the command palette UI:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Command Palette</title>
<link rel="stylesheet" href="styles.css">
</head>
<body>
<div class="command-palette">
<div class="search-container">
<input
type="text"
id="search-input"
placeholder="Type a command or search..."
autocomplete="off"
autofocus
>
</div>
<div class="results-container" id="results">
<!-- Command results will be inserted here -->
</div>
<div class="footer">
<span class="hint">↑↓ Navigate</span>
<span class="hint">↵ Execute</span>
<span class="hint">Esc Close</span>
</div>
</div>
<script src="popup.js"></script>
</body>
</html>
Styling the Command Palette
The styles.css file provides a modern, clean appearance:
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
width: 600px;
min-height: 400px;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: #1e1e1e;
color: #ffffff;
}
.command-palette {
display: flex;
flex-direction: column;
height: 100%;
}
.search-container {
padding: 16px;
border-bottom: 1px solid #333;
}
#search-input {
width: 100%;
padding: 12px 16px;
font-size: 16px;
background: #2d2d2d;
border: 1px solid #404040;
border-radius: 8px;
color: #ffffff;
outline: none;
transition: border-color 0.2s, box-shadow 0.2s;
}
#search-input:focus {
border-color: #007acc;
box-shadow: 0 0 0 3px rgba(0, 122, 204, 0.3);
}
#search-input::placeholder {
color: #808080;
}
.results-container {
flex: 1;
overflow-y: auto;
padding: 8px;
}
.result-item {
display: flex;
align-items: center;
padding: 12px 16px;
margin: 4px 0;
border-radius: 6px;
cursor: pointer;
transition: background-color 0.15s;
}
.result-item:hover,
.result-item.selected {
background: #2d2d2d;
}
.result-item.selected {
background: #007acc;
}
.result-icon {
width: 24px;
height: 24px;
margin-right: 12px;
display: flex;
align-items: center;
justify-content: center;
color: #808080;
}
.result-item.selected .result-icon {
color: #ffffff;
}
.result-content {
flex: 1;
}
.result-title {
font-size: 14px;
font-weight: 500;
margin-bottom: 2px;
}
.result-description {
font-size: 12px;
color: #808080;
}
.result-item.selected .result-description {
color: rgba(255, 255, 255, 0.8);
}
.result-shortcut {
font-size: 12px;
color: #808080;
background: #333;
padding: 2px 6px;
border-radius: 4px;
}
.footer {
display: flex;
justify-content: center;
gap: 16px;
padding: 12px;
border-top: 1px solid #333;
background: #252525;
}
.hint {
font-size: 11px;
color: #808080;
}
.no-results {
text-align: center;
padding: 32px;
color: #808080;
}
Implementing Command Logic
Now let’s implement the JavaScript functionality that makes the command palette work.
Core Command System
The popup.js file handles all the command logic:
// Default commands available in the extension
const defaultCommands = [
{
id: 'new-tab',
title: 'New Tab',
description: 'Open a new browser tab',
icon: '➕',
shortcut: 'Ctrl+T',
action: () => chrome.tabs.create({})
},
{
id: 'new-window',
title: 'New Window',
description: 'Open a new browser window',
icon: '🗗',
shortcut: 'Ctrl+N',
action: () => chrome.windows.create({})
},
{
id: 'close-tab',
title: 'Close Current Tab',
description: 'Close the active tab',
icon: '✖',
shortcut: 'Ctrl+W',
action: async () => {
const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
chrome.tabs.remove(tab.id);
}
},
{
id: 'reopen-closed-tab',
title: 'Reopen Closed Tab',
description: 'Restore the most recently closed tab',
icon: '↩',
action: async () => {
const sessions = await chrome.sessions.getRecentlyClosed({ maxResults: 1 });
if (sessions.length > 0) {
await chrome.sessions.restore(sessions[0].sessionId);
}
}
},
{
id: 'bookmarks',
title: 'Open Bookmarks',
description: 'Access your saved bookmarks',
icon: '🔖',
action: () => chrome.tabs.create({ url: 'chrome://bookmarks' })
},
{
id: 'history',
title: 'View History',
description: 'Browse your browsing history',
icon: '📜',
shortcut: 'Ctrl+H',
action: () => chrome.tabs.create({ url: 'chrome://history' })
},
{
id: 'downloads',
title: 'Downloads',
description: 'View your downloads',
icon: '⬇',
shortcut: 'Ctrl+J',
action: () => chrome.tabs.create({ url: 'chrome://downloads' })
},
{
id: 'extensions',
title: 'Extensions',
description: 'Manage your extensions',
icon: '🧩',
shortcut: 'Ctrl+Shift+E',
action: () => chrome.tabs.create({ url: 'chrome://extensions' })
},
{
id: 'settings',
title: 'Settings',
description: 'Open Chrome settings',
icon: '⚙',
shortcut: 'Ctrl+,',
action: () => chrome.tabs.create({ url: 'chrome://settings' })
},
{
id: 'clear-cache',
title: 'Clear Cache',
description: 'Clear browsing data and cache',
icon: '🗑',
action: () => chrome.tabs.create({ url: 'chrome://settings/clearBrowserData' })
}
];
// State management
let commands = [...defaultCommands];
let selectedIndex = 0;
let filteredCommands = [...commands];
// DOM Elements
const searchInput = document.getElementById('search-input');
const resultsContainer = document.getElementById('results');
// Initialize the extension
function init() {
loadCustomCommands();
renderResults();
searchInput.focus();
// Load custom commands from storage
async function loadCustomCommands() {
try {
const result = await chrome.storage.local.get('customCommands');
if (result.customCommands) {
commands = [...defaultCommands, ...result.customCommands];
filteredCommands = [...commands];
}
} catch (error) {
console.error('Error loading custom commands:', error);
}
}
}
// Filter commands based on search query
function filterCommands(query) {
if (!query.trim()) {
filteredCommands = [...commands];
} else {
const lowerQuery = query.toLowerCase();
filteredCommands = commands.filter(cmd =>
cmd.title.toLowerCase().includes(lowerQuery) ||
cmd.description.toLowerCase().includes(lowerQuery)
);
}
selectedIndex = 0;
renderResults();
}
// Render command results
function renderResults() {
if (filteredCommands.length === 0) {
resultsContainer.innerHTML = '<div class="no-results">No commands found</div>';
return;
}
resultsContainer.innerHTML = filteredCommands.map((cmd, index) => `
<div class="result-item ${index === selectedIndex ? 'selected' : ''}" data-index="${index}">
<div class="result-icon">${cmd.icon}</div>
<div class="result-content">
<div class="result-title">${cmd.title}</div>
<div class="result-description">${cmd.description}</div>
</div>
${cmd.shortcut ? `<span class="result-shortcut">${cmd.shortcut}</span>` : ''}
</div>
`).join('');
// Add click handlers
document.querySelectorAll('.result-item').forEach(item => {
item.addEventListener('click', () => {
const index = parseInt(item.dataset.index);
executeCommand(index);
});
});
}
// Execute selected command
async function executeCommand(index) {
if (index >= 0 && index < filteredCommands.length) {
const command = filteredCommands[index];
try {
await command.action();
window.close();
} catch (error) {
console.error('Error executing command:', error);
}
}
}
// Keyboard navigation
function handleKeydown(e) {
switch (e.key) {
case 'ArrowDown':
e.preventDefault();
selectedIndex = Math.min(selectedIndex + 1, filteredCommands.length - 1);
renderResults();
scrollToSelected();
break;
case 'ArrowUp':
e.preventDefault();
selectedIndex = Math.max(selectedIndex - 1, 0);
renderResults();
scrollToSelected();
break;
case 'Enter':
e.preventDefault();
executeCommand(selectedIndex);
break;
case 'Escape':
window.close();
break;
}
}
// Scroll selected item into view
function scrollToSelected() {
const selected = document.querySelector('.result-item.selected');
if (selected) {
selected.scrollIntoView({ block: 'nearest' });
}
}
// Event Listeners
searchInput.addEventListener('input', (e) => filterCommands(e.target.value));
document.addEventListener('keydown', handleKeydown);
// Initialize on load
document.addEventListener('DOMContentLoaded', init);
Advanced Features
Now let’s explore some advanced features that will make your command palette extension truly powerful.
Adding Page-Specific Commands
A command palette extension can offer different commands depending on the current page. Let’s implement this feature:
// Content script that communicates with the extension
// content.js
// Listen for messages from the extension
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
if (message.type === 'GET_PAGE_COMMANDS') {
const pageCommands = getPageSpecificCommands();
sendResponse(pageCommands);
}
return true;
});
// Get commands specific to the current page
function getPageSpecificCommands() {
const hostname = window.location.hostname;
const commands = [];
// Add page-specific commands based on the current site
if (hostname.includes('github.com')) {
commands.push(
{
id: 'github-issues',
title: 'Go to Issues',
description: 'Navigate to GitHub Issues',
icon: '🐙',
action: () => window.location.href = '/issues'
},
{
id: 'github-pr',
title: 'Go to Pull Requests',
description: 'Navigate to Pull Requests',
icon: '🔀',
action: () => window.location.href = '/pulls'
}
);
}
if (hostname.includes('youtube.com')) {
commands.push(
{
id: 'youtube-subscriptions',
title: 'Go to Subscriptions',
description: 'View subscription feed',
icon: '📺',
action: () => window.location.href = '/feed/subscriptions'
},
{
id: 'youtube-history',
title: 'Watch History',
description: 'View watch history',
icon: '🕐',
action: () => window.location.href = '/feed/history'
}
);
}
return commands;
}
Search Enhancement with Fuzzy Matching
Implement fuzzy search for better command matching:
// Fuzzy search implementation
function fuzzyMatch(text, query) {
const textLower = text.toLowerCase();
const queryLower = query.toLowerCase();
let textIndex = 0;
let queryIndex = 0;
while (textIndex < textLower.length && queryIndex < queryLower.length) {
if (textLower[textIndex] === queryLower[queryIndex]) {
queryIndex++;
}
textIndex++;
}
return queryIndex === queryLower.length;
}
// Improved filter with fuzzy matching
function filterCommandsFuzzy(query) {
if (!query.trim()) {
return commands.map(cmd => ({ ...cmd, score: 0 }));
}
const results = commands
.map(cmd => {
let score = 0;
// Exact match gets highest score
if (cmd.title.toLowerCase().startsWith(query.toLowerCase())) {
score = 100;
} else if (cmd.title.toLowerCase().includes(query.toLowerCase())) {
score = 50;
} else if (fuzzyMatch(cmd.title, query)) {
score = 25;
}
// Description matches add bonus points
if (cmd.description.toLowerCase().includes(query.toLowerCase())) {
score += 10;
}
return { ...cmd, score };
})
.filter(cmd => cmd.score > 0)
.sort((a, b) => b.score - a.score);
return results;
}
Performance Optimization
A command palette needs to be lightning fast. Here are optimization techniques:
Debounced Search
function debounce(func, wait) {
let timeout;
return function executedFunction(...args) {
const later = () => {
clearTimeout(timeout);
func(...args);
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
}
// Apply debounce to search
const debouncedFilter = debounce((query) => {
filteredCommands = filterCommandsFuzzy(query);
selectedIndex = 0;
renderResults();
}, 150);
searchInput.addEventListener('input', (e) => debouncedFilter(e.target.value));
Lazy Loading Icons
// Lazy load command icons
function lazyLoadIcons() {
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const img = entry.target;
img.src = img.dataset.src;
observer.unobserve(img);
}
});
});
document.querySelectorAll('img[data-src]').forEach(img => {
observer.observe(img);
});
}
Testing and Debugging
Proper testing ensures your extension works correctly:
Unit Testing Commands
// Test the filter function
function testFilterCommands() {
const testCases = [
{ query: 'new', expected: ['New Tab', 'New Window'] },
{ query: 'tab', expected: ['New Tab', 'Close Current Tab'] },
{ query: 'xyz', expected: [] }
];
testCases.forEach(({ query, expected }) => {
const results = filterCommands(query);
const titles = results.map(r => r.title);
console.assert(
JSON.stringify(titles) === JSON.stringify(expected),
`Failed for query "${query}": expected ${expected}, got ${titles}`
);
});
}
// Run tests in development
if (process.env.NODE_ENV === 'development') {
testFilterCommands();
}
Best Practices and Tips
Follow these best practices to create a polished command palette extension:
-
Keyboard First Design: Always prioritize keyboard navigation and shortcuts. Most power users prefer keyboard over mouse.
-
Consistent Shortcuts: Use standard Chrome shortcuts where possible (Ctrl+T for new tab, Ctrl+W to close, etc.).
-
Clear Visual Feedback: Provide immediate visual feedback for all interactions, especially selection states.
-
Performance Matters: Keep the interface responsive. Use debouncing and lazy loading to maintain snappy performance.
-
Accessibility: Ensure your command palette works with screen readers and supports keyboard-only navigation.
-
Customizability: Allow users to add their own commands and customize shortcuts.
-
Error Handling: Gracefully handle errors and provide helpful messages when commands fail.
Conclusion
Building a command palette Chrome extension is an excellent way to enhance user productivity and create a more efficient browsing experience. This guide covered the essential components: project setup, interface design, command implementation, advanced features like fuzzy search and page-specific commands, performance optimization, and testing.
The command bar Chrome extension you build can serve as a foundation for even more sophisticated features. Consider adding integrations with third-party APIs, implementing command categories, adding user-defined macros, or integrating with popular web applications.
Remember that the best command palette extensions combine speed, reliability, and extensibility. Start with the basics outlined in this guide, then iterate based on user feedback and your own usage patterns.
By following the patterns and techniques in this guide, you will be well-equipped to create a command palette extension that rivals industry-standard implementations like Spotlight search extension features found in macOS and other productivity tools.