Build a Speed Dial Chrome Extension

24 min read

Build a Speed Dial Chrome Extension

Build a Speed Dial Chrome Extension

Speed dial extensions have become an essential tool for millions of Chrome users who want quick access to their favorite websites. Whether you want to launch your most-visited pages with a single click or organize bookmarks in a visually appealing grid, building a speed dial extension is an excellent project that combines practical functionality with valuable development experience. This comprehensive guide will walk you through creating a fully functional Speed Dial Chrome Extension using Manifest V3, covering everything from project setup to advanced features like drag-and-drop reordering and custom theming.

The concept of speed dial originated in web browsers as a way to provide instant access to bookmarked pages. Today, speed dial extensions have evolved into sophisticated tools that can sync across devices, display thumbnails of favorite sites, and even integrate with productivity apps. By building your own speed dial extension, you gain deep insight into Chrome’s extension architecture while creating a tool that you and others can actually use daily.


Understanding Speed Dial Extensions

A speed dial extension fundamentally serves as a visual launcher for your most frequently visited websites. Unlike traditional bookmarks that hide behind menus, speed dials present your favorite sites as clickable tiles on the new tab page or within the extension popup. This immediate accessibility makes them invaluable for users who need to switch between multiple web applications throughout their day.

Core Features of Speed Dial Extensions

The most successful speed dial extensions share several common features that make them useful for everyday browsing. First and foremost is the grid-based display system that shows website thumbnails or favicons in an organized layout. This visual approach allows users to recognize sites by their appearance rather than remembering URLs or bookmark names.

Quick access functionality represents another critical component. Users should be able to open any speed dial site with a single click, whether they are using the extension popup or the new tab page. This seamless experience eliminates the friction of navigating through multiple menus to reach frequently used resources.

Customization options distinguish premium speed dial extensions from basic implementations. Users typically expect the ability to add or remove dials, rearrange their positions, and customize the visual appearance through themes or background images. Some extensions even support importing existing browser bookmarks to populate the speed dial automatically.


Project Setup and Manifest Configuration

Every Chrome extension begins with the manifest file, which defines the extension’s capabilities and permissions. For our speed dial extension, we will use Manifest V3, which offers improved security and performance compared to older versions.

Creating the Project Structure

First, create a new directory for your speed dial extension project. Within this directory, create the following folder structure:

speed-dial-extension/
├── manifest.json
├── popup/
│   ├── popup.html
│   ├── popup.css
│   └── popup.js
├── newtab/
│   ├── newtab.html
│   ├── newtab.css
│   └── newtab.js
├── background/
│   └── background.js
├── icons/
│   ├── icon16.png
│   ├── icon48.png
│   └── icon128.png
└── styles/
    └── common.css

This structure separates different components of the extension, making it easier to maintain and expand later. The popup folder handles the extension’s popup interface, while the newtab folder contains the new tab page implementation.

Writing the Manifest File

The manifest.json file defines how Chrome loads and interacts with your extension. Here is a complete Manifest V3 configuration for our speed dial extension:

{
  "manifest_version": 3,
  "name": "Quick Access Speed Dial",
  "version": "1.0",
  "description": "A fast and customizable speed dial extension for Chrome",
  "permissions": [
    "storage",
    "bookmarks",
    "topSites"
  ],
  "action": {
    "default_popup": "popup/popup.html",
    "default_icon": {
      "16": "icons/icon16.png",
      "48": "icons/icon48.png",
      "128": "icons/icon128.png"
    }
  },
  "chrome_url_overrides": {
    "newtab": "newtab/newtab.html"
  },
  "background": {
    "service_worker": "background/background.js",
    "type": "module"
  },
  "icons": {
    "16": "icons/icon16.png",
    "48": "icons/icon48.png",
    "128": "icons/icon128.png"
  }
}

The permissions array includes storage for saving user preferences and dial configurations, bookmarks for importing existing browser bookmarks, and topSites for automatically populating the dial with frequently visited pages. The chrome_url_overrides section replaces the default new tab page with our custom speed dial interface.


The popup interface provides quick access to your speed dials without leaving the current page. This lightweight interface appears when users click the extension icon in the Chrome toolbar.

HTML Structure

Create the popup.html file with a clean, grid-based layout:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Quick Access Speed Dial</title>
  <link rel="stylesheet" href="popup.css">
</head>
<body>
  <div class="popup-container">
    <header class="popup-header">
      <h1>Speed Dial</h1>
      <button id="settings-btn" class="icon-btn" aria-label="Settings">
        ⚙️
      </button>
    </header>
    
    <div class="dials-grid" id="dials-grid">
      <!-- Speed dial tiles will be inserted here -->
    </div>
    
    <footer class="popup-footer">
      <button id="add-dial-btn" class="add-btn">+ Add New Dial</button>
    </footer>
  </div>
  
  <div id="edit-modal" class="modal hidden">
    <div class="modal-content">
      <h2>Edit Speed Dial</h2>
      <form id="edit-form">
        <div class="form-group">
          <label for="dial-title">Title</label>
          <input type="text" id="dial-title" name="title" required>
        </div>
        <div class="form-group">
          <label for="dial-url">URL</label>
          <input type="url" id="dial-url" name="url" required>
        </div>
        <div class="form-actions">
          <button type="button" id="cancel-edit" class="btn secondary">Cancel</button>
          <button type="submit" class="btn primary">Save</button>
        </div>
      </form>
    </div>
  </div>
  
  <script src="popup.js"></script>
</body>
</html>

Styling the Popup

The popup.css file provides attractive styling for the interface:

* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}

body {
  width: 400px;
  min-height: 500px;
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
  background: #f5f5f5;
}

.popup-container {
  display: flex;
  flex-direction: column;
  height: 100%;
}

.popup-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 16px;
  background: white;
  border-bottom: 1px solid #e0e0e0;
}

.popup-header h1 {
  font-size: 18px;
  font-weight: 600;
  color: #333;
}

.icon-btn {
  background: none;
  border: none;
  cursor: pointer;
  font-size: 18px;
  padding: 4px;
  border-radius: 4px;
  transition: background 0.2s;
}

.icon-btn:hover {
  background: #f0f0f0;
}

.dials-grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 12px;
  padding: 16px;
  flex: 1;
  overflow-y: auto;
}

.dial-tile {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  background: white;
  border-radius: 8px;
  padding: 12px 8px;
  cursor: pointer;
  transition: transform 0.2s, box-shadow 0.2s;
  min-height: 80px;
}

.dial-tile:hover {
  transform: translateY(-2px);
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}

.dial-icon {
  width: 32px;
  height: 32px;
  border-radius: 8px;
  margin-bottom: 8px;
  object-fit: contain;
}

.dial-title {
  font-size: 11px;
  text-align: center;
  color: #333;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  max-width: 100%;
}

.popup-footer {
  padding: 12px 16px;
  background: white;
  border-top: 1px solid #e0e0e0;
}

.add-btn {
  width: 100%;
  padding: 10px;
  background: #4285f4;
  color: white;
  border: none;
  border-radius: 6px;
  cursor: pointer;
  font-size: 14px;
  font-weight: 500;
  transition: background 0.2s;
}

.add-btn:hover {
  background: #3367d6;
}

.modal {
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  background: rgba(0, 0, 0, 0.5);
  display: flex;
  align-items: center;
  justify-content: center;
}

.modal.hidden {
  display: none;
}

.modal-content {
  background: white;
  padding: 24px;
  border-radius: 12px;
  width: 90%;
  max-width: 360px;
}

.modal-content h2 {
  margin-bottom: 16px;
  font-size: 18px;
}

.form-group {
  margin-bottom: 16px;
}

.form-group label {
  display: block;
  margin-bottom: 6px;
  font-size: 13px;
  font-weight: 500;
  color: #666;
}

.form-group input {
  width: 100%;
  padding: 10px;
  border: 1px solid #ddd;
  border-radius: 6px;
  font-size: 14px;
}

.form-actions {
  display: flex;
  gap: 12px;
  justify-content: flex-end;
}

.btn {
  padding: 8px 16px;
  border-radius: 6px;
  cursor: pointer;
  font-size: 14px;
  font-weight: 500;
}

.btn.primary {
  background: #4285f4;
  color: white;
  border: none;
}

.btn.primary:hover {
  background: #3367d6;
}

.btn.secondary {
  background: white;
  border: 1px solid #ddd;
  color: #666;
}

.btn.secondary:hover {
  background: #f5f5f5;
}

Implementing Popup Functionality

The popup.js file handles all user interactions:

document.addEventListener('DOMContentLoaded', () => {
  loadDials();
  setupEventListeners();
});

let dials = [];
const STORAGE_KEY = 'speed_dials';

async function loadDials() {
  try {
    const result = await chrome.storage.local.get(STORAGE_KEY);
    dials = result[STORAGE_KEY] || [];
    
    // Load default dials if empty
    if (dials.length === 0) {
      dials = getDefaultDials();
      await saveDials();
    }
    
    renderDials();
  } catch (error) {
    console.error('Error loading dials:', error);
  }
}

function getDefaultDials() {
  return [
    { id: 1, title: 'Google', url: 'https://www.google.com', icon: '' },
    { id: 2, title: 'YouTube', url: 'https://www.youtube.com', icon: '' },
    { id: 3, title: 'Gmail', url: 'https://mail.google.com', icon: '' },
    { id: 4, title: 'GitHub', url: 'https://github.com', icon: '' },
    { id: 5, title: 'Twitter', url: 'https://twitter.com', icon: '' },
    { id: 6, title: 'LinkedIn', url: 'https://linkedin.com', icon: '' }
  ];
}

async function saveDials() {
  try {
    await chrome.storage.local.set({ [STORAGE_KEY]: dials });
  } catch (error) {
    console.error('Error saving dials:', error);
  }
}

function renderDials() {
  const grid = document.getElementById('dials-grid');
  grid.innerHTML = '';
  
  dials.forEach((dial, index) => {
    const tile = document.createElement('div');
    tile.className = 'dial-tile';
    tile.innerHTML = `
      <img class="dial-icon" src="${dial.icon || 'data:image/svg+xml,' + encodeURIComponent(getDefaultIcon(dial.title))}" alt="${dial.title}">
      <span class="dial-title">${dial.title}</span>
    `;
    
    tile.addEventListener('click', () => openDial(dial.url));
    tile.addEventListener('contextmenu', (e) => showContextMenu(e, index));
    
    grid.appendChild(tile);
  });
}

function getDefaultIcon(title) {
  return `<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
    <rect width="32" height="32" rx="8" fill="#4285f4"/>
    <text x="16" y="22" text-anchor="middle" fill="white" font-size="14" font-weight="bold">${title.charAt(0).toUpperCase()}</text>
  </svg>`;
}

function openDial(url) {
  chrome.tabs.create({ url: url });
}

function setupEventListeners() {
  document.getElementById('add-dial-btn').addEventListener('click', showAddDialModal);
  document.getElementById('settings-btn').addEventListener('click', openSettings);
  document.getElementById('cancel-edit').addEventListener('click', hideModal);
  document.getElementById('edit-form').addEventListener('submit', handleFormSubmit);
}

function showAddDialModal() {
  const modal = document.getElementById('edit-modal');
  document.getElementById('dial-title').value = '';
  document.getElementById('dial-url').value = '';
  modal.classList.remove('hidden');
}

function hideModal() {
  document.getElementById('edit-modal').classList.add('hidden');
}

async function handleFormSubmit(e) {
  e.preventDefault();
  
  const title = document.getElementById('dial-title').value;
  const url = document.getElementById('dial-url').value;
  
  const newDial = {
    id: Date.now(),
    title: title,
    url: url.startsWith('http') ? url : 'https://' + url,
    icon: ''
  };
  
  dials.push(newDial);
  await saveDials();
  renderDials();
  hideModal();
}

function showContextMenu(e, index) {
  e.preventDefault();
  // Context menu implementation would go here
}

function openSettings() {
  // Settings page navigation
  chrome.tabs.create({ url: 'settings.html' });
}

Creating the New Tab Page

The new tab page provides a full-screen speed dial experience when users open a new tab. This is often the primary interface for speed dial extensions.

New Tab HTML Structure

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Speed Dial</title>
  <link rel="stylesheet" href="newtab.css">
</head>
<body>
  <div class="newtab-container">
    <header class="newtab-header">
      <div class="search-box">
        <input type="text" id="search-input" placeholder="Search the web...">
      </div>
      <button id="edit-mode-btn" class="edit-btn">Edit Dials</button>
    </header>
    
    <main class="dials-container">
      <div class="dials-grid-large" id="dials-grid-large">
        <!-- Large speed dial tiles -->
      </div>
    </main>
    
    <footer class="newtab-footer">
      <span id="date-display"></span>
    </footer>
  </div>
  
  <script src="newtab.js"></script>
</body>
</html>

New Tab Styling

body {
  margin: 0;
  padding: 0;
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
  min-height: 100vh;
}

.newtab-container {
  display: flex;
  flex-direction: column;
  min-height: 100vh;
  padding: 20px;
}

.newtab-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 20px 40px;
}

.search-box input {
  width: 500px;
  padding: 12px 20px;
  border: none;
  border-radius: 24px;
  font-size: 16px;
  box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
  outline: none;
}

.search-box input:focus {
  box-shadow: 0 4px 20px rgba(0, 0, 0, 0.2);
}

.edit-btn {
  padding: 10px 20px;
  background: rgba(255, 255, 255, 0.2);
  color: white;
  border: 1px solid rgba(255, 255, 255, 0.3);
  border-radius: 20px;
  cursor: pointer;
  font-size: 14px;
  transition: background 0.2s;
}

.edit-btn:hover {
  background: rgba(255, 255, 255, 0.3);
}

.dials-container {
  flex: 1;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 40px;
}

.dials-grid-large {
  display: grid;
  grid-template-columns: repeat(6, 1fr);
  gap: 20px;
  max-width: 1000px;
}

.dial-tile-large {
  display: flex;
  flex-direction: column;
  align-items: center;
  background: rgba(255, 255, 255, 0.95);
  border-radius: 12px;
  padding: 20px;
  cursor: pointer;
  transition: transform 0.2s, box-shadow 0.2s;
  min-height: 100px;
}

.dial-tile-large:hover {
  transform: scale(1.05);
  box-shadow: 0 8px 30px rgba(0, 0, 0, 0.2);
}

.dial-icon-large {
  width: 48px;
  height: 48px;
  border-radius: 12px;
  margin-bottom: 12px;
}

.dial-title-large {
  font-size: 13px;
  text-align: center;
  color: #333;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  max-width: 100%;
}

.newtab-footer {
  text-align: center;
  padding: 20px;
  color: rgba(255, 255, 255, 0.8);
  font-size: 14px;
}

New Tab JavaScript

document.addEventListener('DOMContentLoaded', () => {
  initializeNewTab();
  displayDate();
  loadDialsForNewTab();
});

function initializeNewTab() {
  const searchInput = document.getElementById('search-input');
  
  searchInput.addEventListener('keypress', (e) => {
    if (e.key === 'Enter') {
      performSearch(searchInput.value);
    }
  });
  
  document.getElementById('edit-mode-btn').addEventListener('click', () => {
    // Toggle edit mode
  });
}

function displayDate() {
  const dateDisplay = document.getElementById('date-display');
  const now = new Date();
  const options = { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' };
  dateDisplay.textContent = now.toLocaleDateString('en-US', options);
}

function performSearch(query) {
  if (query) {
    chrome.tabs.create({ url: `https://www.google.com/search?q=${encodeURIComponent(query)}` });
  }
}

async function loadDialsForNewTab() {
  try {
    const result = await chrome.storage.local.get('speed_dials');
    const dials = result.speed_dials || [];
    renderLargeDials(dials);
  } catch (error) {
    console.error('Error loading dials:', error);
  }
}

function renderLargeDials(dials) {
  const grid = document.getElementById('dials-grid-large');
  grid.innerHTML = '';
  
  dials.forEach(dial => {
    const tile = document.createElement('div');
    tile.className = 'dial-tile-large';
    tile.innerHTML = `
      <img class="dial-icon-large" src="${dial.icon || 'data:image/svg+xml,' + encodeURIComponent(getDefaultIcon(dial.title))}" alt="${dial.title}">
      <span class="dial-title-large">${dial.title}</span>
    `;
    
    tile.addEventListener('click', () => {
      chrome.tabs.create({ url: dial.url });
    });
    
    grid.appendChild(tile);
  });
}

function getDefaultIcon(title) {
  return `<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 48 48">
    <rect width="48" height="48" rx="12" fill="#4285f4"/>
    <text x="24" y="32" text-anchor="middle" fill="white" font-size="20" font-weight="bold">${title.charAt(0).toUpperCase()}</text>
  </svg>`;
}

Advanced Features and Enhancements

Now that you have a functional speed dial extension, consider adding these advanced features to make it more powerful and user-friendly.

Drag and Drop Reordering

Implementing drag and drop allows users to organize their speed dials intuitively. You can use the HTML5 Drag and Drop API to enable this functionality. The key is to track the position of each dial tile and update the storage when the user rearranges them.

Automatic Thumbnail Generation

Instead of using static icons, you can generate live thumbnails of websites using the chrome.tabCapture API or by taking screenshots of loaded pages. This provides users with visual confirmation of their bookmarks and makes the interface more engaging.

Background Sync and Cloud Storage

Integrating cloud synchronization allows users to access their speed dial configurations across multiple devices. You can implement this using a backend service or leverage Chrome’s sync storage API which automatically syncs data across all devices where the user is signed in.

Import from Bookmarks

The bookmarks permission allows importing existing browser bookmarks into your speed dial. This is an excellent feature for users who want to migrate their bookmarked sites to a speed dial interface without manually re-entering each URL.


Testing and Debugging Your Extension

Before distributing your extension, thorough testing ensures a smooth user experience. Load your extension in developer mode through chrome://extensions, enable developer mode, and use the “Load unpacked” button to select your extension directory.

Test all core functionality including adding and removing dials, opening URLs, and the new tab override. Pay special attention to edge cases like very long titles or invalid URLs that might cause display issues.


Publishing Your Extension

Once testing is complete, you can publish your extension to the Chrome Web Store. Create a developer account, package your extension using the “Pack extension” button in chrome://extensions, and upload the package through the Chrome Web Store developer dashboard. Ensure your listing includes clear descriptions, appropriate screenshots, and relevant keywords to improve discoverability.

Building a speed dial extension teaches valuable skills in Chrome extension development while creating a genuinely useful tool. The foundation you have built in this guide can be extended with countless features to make your extension stand out in the Chrome Web Store.

No previous article
No next article