Build a Clipboard History Manager Chrome Extension
If you have ever copied something important only to lose it when you copied something else, you understand the frustration of a one-item clipboard. A clipboard history extension solves this problem by storing every item you copy, allowing you to retrieve and reuse previous selections instantly. Building your own clipboard history manager Chrome extension is an excellent project that teaches you essential Chrome extension development concepts while creating a genuinely useful tool.
This comprehensive guide walks you through building a complete clipboard history extension using Manifest V3, the latest Chrome extension platform. You will learn how to capture clipboard content, store history efficiently, design an intuitive user interface, and implement advanced features like search and favorites. By the end of this guide, you will have a fully functional clipboard manager ready for personal use or publication to the Chrome Web Store.
Understanding Clipboard API in Chrome Extensions
Chrome provides several APIs for working with the clipboard, each with specific use cases and limitations. Understanding these APIs is crucial before implementing your clipboard history extension.
The Clipboard API Methods
Modern Chrome extensions can interact with the clipboard using the navigator.clipboard API, which provides asynchronous read and write operations. The readText() method retrieves text content from the clipboard, while writeText() sets clipboard content. These Promise-based methods offer a clean, modern approach to clipboard manipulation.
// Reading from clipboard
async function readClipboard() {
try {
const text = await navigator.clipboard.readText();
return text;
} catch (err) {
console.error('Failed to read clipboard:', err);
return null;
}
}
// Writing to clipboard
async function writeToClipboard(text) {
try {
await navigator.clipboard.writeText(text);
return true;
} catch (err) {
console.error('Failed to write to clipboard:', err);
return false;
}
}
However, the Clipboard API has an important limitation: it requires the page to be focused to read from the clipboard. This restriction exists for security reasons, preventing websites from silently reading sensitive data users have copied. For a background clipboard monitor, you need a different approach.
Using the Clipboard Board Access Permission
In Manifest V3, extensions can request “clipboardRead” and “clipboardWrite” permissions to access clipboard content more freely. Add these permissions to your manifest.json to enable background clipboard monitoring:
{
"permissions": [
"clipboardRead",
"clipboardWrite",
"storage",
"notifications"
],
"host_permissions": [
"<all_urls>"
]
}
The “clipboardRead” permission allows your extension to read clipboard content even when the user is not directly interacting with your popup. This is essential for automatically capturing copied items in the background.
Project Structure and Manifest Configuration
Every Chrome extension begins with a well-structured manifest.json file. For a clipboard history manager, you need to configure several key components.
The Manifest V3 Configuration
Create a new folder for your extension and add the manifest.json file with all necessary configurations:
{
"manifest_version": 3,
"name": "Clipboard History Manager",
"version": "1.0.0",
"description": "Never lose copied content again. Store and retrieve your complete clipboard history.",
"permissions": [
"clipboardRead",
"clipboardWrite",
"storage",
"notifications"
],
"host_permissions": [
"<all_urls>"
],
"background": {
"service_worker": "background.js"
},
"action": {
"default_popup": "popup.html",
"default_icon": {
"16": "icons/icon16.png",
"48": "icons/icon48.png",
"128": "icons/icon128.png"
}
},
"icons": {
"16": "icons/icon16.png",
"48": "icons/icon48.png",
"128": "icons/icon128.png"
}
}
This configuration establishes the foundation for your clipboard history extension. The background service worker handles clipboard monitoring, while the popup provides the user interface for accessing history.
Directory Structure
Organize your extension files logically to maintain code clarity:
clipboard-history/
├── manifest.json
├── background.js
├── popup.html
├── popup.js
├── styles.css
├── icons/
│ ├── icon16.png
│ ├── icon48.png
│ └── icon128.png
└── content.js
This structure separates concerns effectively: background scripts handle logic, popup files manage the user interface, and the icons folder contains your extension’s visual assets.
Implementing the Background Clipboard Monitor
The background service worker is the heart of your clipboard history extension. It monitors the clipboard continuously and stores new entries automatically.
Setting Up the Service Worker
Create background.js with the clipboard monitoring logic:
// background.js
let clipboardHistory = [];
let lastClipboardContent = '';
let monitorInterval = null;
// Initialize extension
chrome.runtime.onInstalled.addListener(() => {
// Load existing history from storage
chrome.storage.local.get(['clipboardHistory'], (result) => {
if (result.clipboardHistory) {
clipboardHistory = result.clipboardHistory;
}
});
// Start monitoring clipboard
startClipboardMonitor();
});
// Monitor clipboard at regular intervals
function startClipboardMonitor() {
monitorInterval = setInterval(async () => {
try {
const clipboardText = await navigator.clipboard.readText();
// Check if content has changed and is not empty
if (clipboardText && clipboardText !== lastClipboardContent) {
lastClipboardContent = clipboardText;
addToHistory(clipboardText);
}
} catch (err) {
// Clipboard access may fail when page is focused
// This is expected behavior, no action needed
}
}, 1000); // Check every second
}
// Add new item to clipboard history
function addToHistory(content) {
const newItem = {
id: Date.now(),
content: content,
timestamp: new Date().toISOString(),
favorite: false
};
// Add to beginning of array
clipboardHistory.unshift(newItem);
// Limit history to 100 items
if (clipboardHistory.length > 100) {
clipboardHistory.pop();
}
// Save to storage
chrome.storage.local.set({ clipboardHistory });
// Show notification for important copies (optional)
if (content.length > 100) {
chrome.notifications.create({
type: 'basic',
iconUrl: 'icons/icon48.png',
title: 'Clipboard Saved',
message: 'New item added to clipboard history'
});
}
}
// Handle messages from popup
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
if (request.action === 'getHistory') {
sendResponse({ history: clipboardHistory });
} else if (request.action === 'copyItem') {
navigator.clipboard.writeText(request.content);
sendResponse({ success: true });
} else if (request.action === 'deleteItem') {
clipboardHistory = clipboardHistory.filter(item => item.id !== request.id);
chrome.storage.local.set({ clipboardHistory });
sendResponse({ success: true });
} else if (request.action === 'toggleFavorite') {
const item = clipboardHistory.find(item => item.id === request.id);
if (item) {
item.favorite = !item.favorite;
chrome.storage.local.set({ clipboardHistory });
}
sendResponse({ success: true });
}
});
This background script continuously monitors the clipboard, captures new content, and stores it efficiently. The service worker runs in the background, ensuring your history keeps growing even when no popup is open.
Building the Popup Interface
The popup provides users with access to their clipboard history. Design it to be clean, responsive, and intuitive.
The HTML Structure
Create popup.html with a well-organized interface:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Clipboard History</title>
<link rel="stylesheet" href="styles.css">
</head>
<body>
<div class="container">
<header>
<h1>Clipboard History</h1>
<div class="search-box">
<input type="text" id="searchInput" placeholder="Search history...">
</div>
</header>
<div class="tabs">
<button class="tab active" data-tab="all">All</button>
<button class="tab" data-tab="favorites">Favorites</button>
</div>
<div id="historyList" class="history-list">
<!-- Clipboard items will be inserted here -->
</div>
<footer>
<button id="clearAll" class="btn-secondary">Clear All</button>
<span id="itemCount">0 items</span>
</footer>
</div>
<script src="popup.js"></script>
</body>
</html>
Styling the Interface
Create styles.css to make your popup visually appealing:
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
width: 350px;
min-height: 400px;
background: #f5f5f5;
}
.container {
display: flex;
flex-direction: column;
height: 100%;
}
header {
background: #4285f4;
color: white;
padding: 16px;
}
header h1 {
font-size: 18px;
margin-bottom: 12px;
}
.search-box input {
width: 100%;
padding: 8px 12px;
border: none;
border-radius: 4px;
font-size: 14px;
}
.tabs {
display: flex;
background: white;
border-bottom: 1px solid #e0e0e0;
}
.tab {
flex: 1;
padding: 12px;
border: none;
background: none;
cursor: pointer;
font-size: 14px;
color: #666;
}
.tab.active {
color: #4285f4;
border-bottom: 2px solid #4285f4;
}
.history-list {
flex: 1;
overflow-y: auto;
padding: 8px;
}
.history-item {
background: white;
border-radius: 8px;
padding: 12px;
margin-bottom: 8px;
cursor: pointer;
transition: box-shadow 0.2s;
}
.history-item:hover {
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
.history-item.favorite {
border-left: 3px solid #fbbc04;
}
.item-content {
font-size: 13px;
color: #333;
max-height: 60px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.item-meta {
display: flex;
justify-content: space-between;
margin-top: 8px;
font-size: 11px;
color: #999;
}
.item-actions {
display: flex;
gap: 8px;
}
.item-actions button {
background: none;
border: none;
cursor: pointer;
color: #666;
padding: 4px;
}
.item-actions button:hover {
color: #4285f4;
}
footer {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 16px;
background: white;
border-top: 1px solid #e0e0e0;
}
.btn-secondary {
padding: 6px 12px;
background: #f5f5f5;
border: 1px solid #ddd;
border-radius: 4px;
cursor: pointer;
font-size: 12px;
}
.btn-secondary:hover {
background: #e0e0e0;
}
#itemCount {
font-size: 12px;
color: #666;
}
Implementing Popup Logic
Create popup.js to handle user interactions:
document.addEventListener('DOMContentLoaded', () => {
loadHistory();
setupEventListeners();
});
let currentTab = 'all';
let allHistory = [];
function loadHistory() {
chrome.runtime.sendMessage({ action: 'getHistory' }, (response) => {
allHistory = response.history || [];
renderHistory();
});
}
function renderHistory() {
const historyList = document.getElementById('historyList');
const itemCount = document.getElementById('itemCount');
// Filter by tab
let filteredHistory = allHistory;
if (currentTab === 'favorites') {
filteredHistory = allHistory.filter(item => item.favorite);
}
// Get search query
const searchQuery = document.getElementById('searchInput').value.toLowerCase();
if (searchQuery) {
filteredHistory = filteredHistory.filter(item =>
item.content.toLowerCase().includes(searchQuery)
);
}
itemCount.textContent = `${filteredHistory.length} items`;
// Render items
if (filteredHistory.length === 0) {
historyList.innerHTML = '<div class="empty-state">No clipboard history</div>';
return;
}
historyList.innerHTML = filteredHistory.map(item => `
<div class="history-item ${item.favorite ? 'favorite' : ''}" data-id="${item.id}">
<div class="item-content">${escapeHtml(item.content)}</div>
<div class="item-meta">
<span>${formatDate(item.timestamp)}</span>
<div class="item-actions">
<button class="copy-btn" title="Copy">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>
</svg>
</button>
<button class="favorite-btn" title="${item.favorite ? 'Remove from favorites' : 'Add to favorites'}">
<svg width="14" height="14" viewBox="0 0 24 24" fill="${item.favorite ? 'currentColor' : 'none'}" stroke="currentColor" stroke-width="2">
<polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"></polygon>
</svg>
</button>
<button class="delete-btn" title="Delete">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="3 6 5 6 21 6"></polyline>
<path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path>
</svg>
</button>
</div>
</div>
</div>
`).join('');
// Add click handlers
document.querySelectorAll('.history-item').forEach(item => {
const id = parseInt(item.dataset.id);
item.querySelector('.copy-btn').addEventListener('click', (e) => {
e.stopPropagation();
copyItem(id);
});
item.querySelector('.favorite-btn').addEventListener('click', (e) => {
e.stopPropagation();
toggleFavorite(id);
});
item.querySelector('.delete-btn').addEventListener('click', (e) => {
e.stopPropagation();
deleteItem(id);
});
item.addEventListener('click', () => {
copyItem(id);
});
});
}
function setupEventListeners() {
// Tab switching
document.querySelectorAll('.tab').forEach(tab => {
tab.addEventListener('click', () => {
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
tab.classList.add('active');
currentTab = tab.dataset.tab;
renderHistory();
});
});
// Search
document.getElementById('searchInput').addEventListener('input', renderHistory);
// Clear all
document.getElementById('clearAll').addEventListener('click', () => {
if (confirm('Clear all clipboard history?')) {
chrome.storage.local.set({ clipboardHistory: [] });
loadHistory();
}
});
}
function copyItem(id) {
const item = allHistory.find(i => i.id === id);
if (item) {
chrome.runtime.sendMessage({
action: 'copyItem',
content: item.content
});
}
}
function toggleFavorite(id) {
chrome.runtime.sendMessage({ action: 'toggleFavorite', id }, () => {
loadHistory();
});
}
function deleteItem(id) {
chrome.runtime.sendMessage({ action: 'deleteItem', id }, () => {
loadHistory();
});
}
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
function formatDate(timestamp) {
const date = new Date(timestamp);
const now = new Date();
const diff = now - date;
if (diff < 60000) return 'Just now';
if (diff < 3600000) return `${Math.floor(diff / 60000)}m ago`;
if (diff < 86400000) return `${Math.floor(diff / 3600000)}h ago`;
return date.toLocaleDateString();
}
Advanced Features and Optimization
A basic clipboard history extension works well, but adding advanced features elevates your extension from useful to exceptional.
Implementing Search Functionality
The search feature already included in the popup code provides powerful filtering capabilities. Users can quickly find specific copied items by typing keywords, dates, or phrases. This is especially valuable when the clipboard history grows large.
Favorites System
Allowing users to mark important clipboard items as favorites prevents them from being accidentally deleted or pushed out of the history when the storage limit is reached. The implementation stores a boolean flag with each item and provides filtering by favorite status.
Keyboard Shortcuts
Adding keyboard shortcuts improves user experience significantly:
// Add to background.js
chrome.commands.onCommand.addListener((command) => {
if (command === 'open-clipboard-history') {
chrome.action.openPopup();
}
});
Configure the shortcut in manifest.json:
"commands": {
"open-clipboard-history": {
"suggested_key": {
"default": "Ctrl+Shift+V",
"mac": "Command+Shift+V"
},
"description": "Open clipboard history"
}
}
Data Persistence and Storage Optimization
Chrome provides multiple storage options for extensions. For clipboard history, consider using chrome.storage.local which offers more storage space than sessionStorage. The current implementation limits history to 100 items, but you can increase this based on user preferences.
Privacy Considerations
Clipboard history extensions handle sensitive data, so privacy is paramount. Implement these best practices:
Never transmit clipboard data to external servers. Store all data locally using chrome.storage.local. Provide users with options to clear history automatically or exclude specific websites from triggering clipboard capture. Be transparent about what data your extension collects and how it is used.
Testing and Debugging Your Extension
Before publishing your extension, thorough testing ensures a smooth user experience.
Loading the Extension Locally
Navigate to chrome://extensions/ in Chrome, enable “Developer mode” in the top right corner, and click “Load unpacked”. Select your extension folder to install it locally for testing.
Common Issues and Solutions
Clipboard access failures often occur when testing because Chrome restricts clipboard reading from certain contexts. Ensure your extension has the proper permissions and test in appropriate environments. Memory leaks can develop if your background script does not properly clean up intervals or event listeners. Use the Chrome DevTools memory profiler to identify potential issues.
Storage quotas may limit how much clipboard data you can store. Monitor storage usage and implement cleanup strategies for older items when approaching limits.
Debugging Tips
Use console.log statements throughout your code during development. The background service worker logs appear in the Extensions page under “Service Worker” after clicking “Inspect views”. The popup logs appear in the popup DevTools when you right-click the popup and select “Inspect”.
Conclusion: Your Complete Clipboard History Extension
You have now built a fully functional clipboard history manager Chrome extension. The extension monitors the clipboard in the background, stores up to 100 items, provides search and favorites functionality, and presents everything in a clean, intuitive interface.
This project demonstrates several essential Chrome extension development concepts that apply to many other extension types. You learned how to work with the Clipboard API, manage background service workers, build popup interfaces, use Chrome storage APIs, and implement user interaction patterns.
The extension you created follows Manifest V3 best practices and is ready for personal use. When you are ready to share it with others, the Chrome Web Store provides a straightforward publishing process. Simply package your extension, create a developer account, and submit your extension for review.
Remember to continue iterating on your extension based on user feedback. Additional features like cloud sync, categories for different content types, and integration with other productivity tools can make your clipboard manager even more powerful. The foundation you built today provides the perfect starting point for these enhancements.
Start using your clipboard history extension today and never lose an important copied item again. Your productivity will thank you.
For more guides on Chrome extension development and optimization, explore our comprehensive documentation and tutorials.
Turn Your Extension Into a Business
Ready to monetize? The Extension Monetization Playbook covers freemium models, Stripe integration, subscription architecture, and growth strategies for Chrome extension developers.