Build a Quick File Upload Extension (2026)
Building a Chrome extension for quick file uploads can dramatically streamline your workflow. Whether you need to share screenshots instantly, upload documents to cloud storage, or transfer files between devices, a well-designed extension transforms a multi-step process into a single click. This guide walks you through the technical implementation and practical considerations for creating file-sharing extensions, from project setup through production-ready security and advanced features.
Understanding the Architecture
Chrome extensions operate within a sandboxed environment with specific permissions. For file sharing functionality, you’ll interact with several Chrome APIs: the File System Access API for reading files, the Downloads API for saving, and various messaging APIs for communication between your extension’s components.
The typical architecture consists of three main parts: a background script that handles core logic, a popup UI for quick actions, and a content script for page-level interactions. For file sharing specifically, you’ll want to focus on the popup and background script interaction, since users will primarily interact through the extension icon in the toolbar.
Understanding the communication model is essential. In Manifest V3, the background script runs as a service worker, which means it can be terminated at any time and must not rely on persistent state. The popup communicates with the background via chrome.runtime.sendMessage, while the background script can use chrome.storage to persist data across sessions.
| Component | Role | Lifecycle |
|---|---|---|
| Service Worker (background.js) | Core upload logic, API calls | Ephemeral, event-driven |
| Popup (popup.html/js) | User interface, file selection | Alive while popup is open |
| Content Script | Page-level DOM access | Alive while tab is open |
| chrome.storage | Persistent configuration | Always available |
For most file-sharing extensions, you do not need a content script at all. The popup plus service worker combination handles the majority of use cases cleanly.
Setting Up Your Manifest
Every Chrome extension begins with the manifest file. For file handling capabilities, you’ll need version 3 of the manifest format:
{
"manifest_version": 3,
"name": "Quick Upload",
"version": "1.0",
"description": "Instantly upload files to your preferred cloud storage",
"permissions": [
"downloads",
"storage",
"clipboardWrite",
"notifications"
],
"host_permissions": [
"https://your-upload-api.example.com/*"
],
"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": {
"quick-screenshot": {
"suggested_key": {
"default": "Ctrl+Shift+U",
"mac": "Command+Shift+U"
},
"description": "Capture and upload a screenshot"
}
}
}
A few notes on this manifest: the fileSystem permission has been removed because showOpenFilePicker does not actually require it in modern Chrome, the File System Access API is available in popup contexts without explicit permission. The host_permissions entry is critical; without it, your fetch() calls to external endpoints will be blocked by CORS policy. The clipboardWrite permission allows you to automatically copy share URLs to the clipboard after a successful upload.
Structuring Your Project Files
A clean file layout prevents confusion as the extension grows:
quick-upload/
manifest.json
popup.html
popup.js
background.js
styles.css
icons/
icon16.png
icon48.png
icon128.png
The popup HTML should be minimal, just enough to display a drag-and-drop zone, a file picker button, an upload progress indicator, and a recent-uploads list:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<link rel="stylesheet" href="styles.css">
</head>
<body>
<div id="drop-zone">
<p>Drop files here or</p>
<button id="pick-files">Choose Files</button>
</div>
<div id="progress-container" class="hidden">
<div id="progress-bar"></div>
<span id="progress-label">Uploading...</span>
</div>
<ul id="recent-uploads"></ul>
<script src="popup.js"></script>
</body>
</html>
Keeping the popup HTML thin and putting all logic in popup.js makes testing easier and allows you to reuse functions between the popup and service worker.
Implementing File Selection
With the manifest configured, you can now implement file selection in your popup. The File System Access API provides a clean way to open the file picker:
// popup.js
async function selectFiles() {
try {
const handles = await window.showOpenFilePicker({
multiple: true,
types: [{
description: 'Images and Documents',
accept: {
'image/*': ['.png', '.jpg', '.jpeg', '.gif', '.webp'],
'application/pdf': ['.pdf'],
'text/*': ['.txt', '.md', '.json', '.csv']
}
}]
});
const files = await Promise.all(
handles.map(handle => handle.getFile())
);
return files;
} catch (error) {
if (error.name !== 'AbortError') {
console.error('File selection error:', error);
}
return [];
}
}
This implementation supports multiple file selection and filters by file type. The showOpenFilePicker method returns file handles rather than direct file data, which is more memory-efficient for large files.
You can also wire up drag-and-drop directly in the popup, giving users two convenient entry points:
// popup.js. drag and drop support
const dropZone = document.getElementById('drop-zone');
dropZone.addEventListener('dragover', (e) => {
e.preventDefault();
dropZone.classList.add('drag-active');
});
dropZone.addEventListener('dragleave', () => {
dropZone.classList.remove('drag-active');
});
dropZone.addEventListener('drop', async (e) => {
e.preventDefault();
dropZone.classList.remove('drag-active');
const files = Array.from(e.dataTransfer.files);
if (files.length > 0) {
await handleUpload(files);
}
});
document.getElementById('pick-files').addEventListener('click', async () => {
const files = await selectFiles();
if (files.length > 0) {
await handleUpload(files);
}
});
Building the Upload Handler
Once you have file handles, the next step is processing and uploading. You’ll want to create a flexible upload function that can work with various backends:
// background.js
async function uploadFiles(files, endpoint) {
const formData = new FormData();
files.forEach((file, index) => {
formData.append(`file_${index}`, file, file.name);
});
const response = await fetch(endpoint, {
method: 'POST',
body: formData
});
if (!response.ok) {
throw new Error(`Upload failed: ${response.status} ${response.statusText}`);
}
return response.json();
}
For the upload endpoint, you have several options. You could use a cloud storage API like AWS S3 or Google Cloud Storage, a service like Uploadcare or Filestack, or your own backend. The key consideration is CORS configuration, your server must accept cross-origin requests from the extension’s origin, which will look like chrome-extension://[extension-id].
Comparing Upload Backend Options
| Backend | Best For | Setup Complexity | Cost |
|---|---|---|---|
| AWS S3 (presigned URLs) | Large files, enterprise | Medium | Pay per use |
| Google Cloud Storage | GCP users | Medium | Pay per use |
| Cloudflare R2 | Cost-sensitive projects | Low | Very cheap |
| Uploadcare | Quick prototypes | Low | Free tier |
| Custom Express/FastAPI | Full control | High | Your infra |
For a quick prototype, Uploadcare’s free tier lets you upload directly from the browser without any server-side code, you include your public key in the extension and files go straight to their CDN. For production use, presigned S3 URLs are the gold standard: the extension requests a presigned URL from your backend, then uploads directly to S3 without routing the file through your server at all.
// Using presigned S3 URLs (recommended production pattern)
async function uploadWithPresignedUrl(file, backendEndpoint) {
// Step 1: Get presigned URL from your backend
const response = await fetch(`${backendEndpoint}/presign`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
filename: file.name,
contentType: file.type,
size: file.size
})
});
const { uploadUrl, fileUrl } = await response.json();
// Step 2: Upload directly to S3
await fetch(uploadUrl, {
method: 'PUT',
body: file,
headers: { 'Content-Type': file.type }
});
return fileUrl;
}
Handling Large Files
When dealing with files larger than a few megabytes, consider implementing chunked uploads. This approach divides the file into smaller pieces, uploads each independently, and reassembles them on the server:
async function chunkedUpload(file, endpoint, chunkSize = 5 * 1024 * 1024) {
const totalChunks = Math.ceil(file.size / chunkSize);
const fileId = crypto.randomUUID();
for (let i = 0; i < totalChunks; i++) {
const start = i * chunkSize;
const end = Math.min(start + chunkSize, file.size);
const chunk = file.slice(start, end);
const formData = new FormData();
formData.append('chunk', chunk);
formData.append('fileId', fileId);
formData.append('chunkIndex', i);
formData.append('totalChunks', totalChunks);
formData.append('filename', file.name);
await fetch(`${endpoint}/chunk`, {
method: 'POST',
body: formData
});
// Report progress back to popup
const progress = Math.round(((i + 1) / totalChunks) * 100);
chrome.runtime.sendMessage({ type: 'UPLOAD_PROGRESS', progress });
}
// Finalize the multipart upload on the server
const finalizeResponse = await fetch(`${endpoint}/finalize`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ fileId, filename: file.name })
});
return finalizeResponse.json();
}
Chunked uploads provide better reliability for large files and allow users to resume interrupted transfers. They also let you report granular progress back to the UI, which significantly improves perceived performance.
In your popup, listen for progress messages and update the progress bar:
// popup.js. progress listener
chrome.runtime.onMessage.addListener((message) => {
if (message.type === 'UPLOAD_PROGRESS') {
const bar = document.getElementById('progress-bar');
const label = document.getElementById('progress-label');
bar.style.width = `${message.progress}%`;
label.textContent = `Uploading... ${message.progress}%`;
}
});
Adding Quick Share Functionality
For truly quick uploads, implement a keyboard shortcut that captures screenshots or selected text. Chrome provides the captureVisibleTab API for taking screenshots of the active tab, which is simpler than the Desktop Capture API for most use cases:
// background.js. keyboard shortcut handler
chrome.commands.onCommand.addListener(async (command) => {
if (command === 'quick-screenshot') {
try {
// Capture the current tab
const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
const dataUrl = await chrome.tabs.captureVisibleTab(tab.windowId, {
format: 'png',
quality: 90
});
// Convert data URL to blob
const response = await fetch(dataUrl);
const blob = await response.blob();
const file = new File([blob], `screenshot-${Date.now()}.png`, {
type: 'image/png'
});
// Upload and get share URL
const { url } = await uploadWithPresignedUrl(file, YOUR_BACKEND_URL);
// Copy URL to clipboard
await navigator.clipboard.writeText(url);
// Notify user
chrome.notifications.create({
type: 'basic',
iconUrl: 'icons/icon48.png',
title: 'Screenshot uploaded',
message: 'Share URL copied to clipboard'
});
} catch (error) {
console.error('Screenshot capture failed:', error);
}
}
});
Combine this with the clipboard API to immediately copy the resulting URL after upload, creating a smooth one-shortcut workflow: press the keys, screenshot is uploaded, URL is in your clipboard, ready to paste.
For selecting specific regions of the screen rather than the full visible tab, you can inject a content script that draws an SVG overlay and lets the user drag a selection rectangle. This is more complex but matches the UX of tools like ShareX or Lightshot.
Managing Recent Uploads
Users should be able to see and re-share their recent uploads without re-uploading. Use chrome.storage.local to persist the history:
// background.js. save upload to history
async function saveToHistory(fileInfo) {
const { history = [] } = await chrome.storage.local.get('history');
history.unshift({
name: fileInfo.name,
url: fileInfo.url,
size: fileInfo.size,
uploadedAt: Date.now()
});
// Keep only the last 20 uploads
const trimmed = history.slice(0, 20);
await chrome.storage.local.set({ history: trimmed });
}
// popup.js. render history
async function renderHistory() {
const { history = [] } = await chrome.storage.local.get('history');
const list = document.getElementById('recent-uploads');
list.innerHTML = history.map(item => `
<li>
<a href="${item.url}" target="_blank">${item.name}</a>
<button data-url="${item.url}" class="copy-btn">Copy</button>
</li>
`).join('');
list.querySelectorAll('.copy-btn').forEach(btn => {
btn.addEventListener('click', () => {
navigator.clipboard.writeText(btn.dataset.url);
});
});
}
This gives users a lightweight clipboard history for their uploads without requiring any additional infrastructure.
Security Considerations
When building file-sharing extensions, security should be at the forefront. Implement these practices:
- Validate file types on both client and server sides. Client-side validation is a UX convenience; server-side validation is the actual security gate. Never trust file extensions alone, check MIME types and, for images, validate the actual file headers.
- Scan uploads for malware if your backend supports it. Services like VirusTotal offer an API, and major cloud storage providers have built-in scanning options.
- Use HTTPS exclusively for all uploads. Never transmit file data over plain HTTP, and verify that your presigned URLs use TLS.
- Implement size limits on both the client (to give fast feedback) and the server (to prevent abuse). A 50MB limit is reasonable for most general-purpose sharing tools.
- Store credentials securely using
chrome.storage.sessionrather thanchrome.storage.localfor API keys or tokens that should not persist across browser restarts. For long-lived credentials, consider usingchrome.identitywith OAuth rather than hardcoding API keys. - Audit your host_permissions. Only list the exact domains your extension needs to contact. Broad permissions like
https://*/*trigger additional scrutiny during Chrome Web Store review.
// Secure credential storage pattern
async function getApiToken() {
// Try session storage first (cleared on browser restart)
const session = await chrome.storage.session.get('apiToken');
if (session.apiToken) return session.apiToken;
// Fall back to OAuth flow via chrome.identity
const token = await new Promise((resolve, reject) => {
chrome.identity.getAuthToken({ interactive: true }, (token) => {
if (chrome.runtime.lastError) {
reject(new Error(chrome.runtime.lastError.message));
} else {
resolve(token);
}
});
});
await chrome.storage.session.set({ apiToken: token });
return token;
}
Practical Use Cases
For developers, quick file upload extensions integrate with development workflows. Share code snippets instantly, upload build artifacts to testing environments, or transfer configuration files between projects. The key advantage is eliminating context switching, instead of navigating to a web interface, logging in, and clicking through upload dialogs, you trigger everything from a keyboard shortcut.
Power users benefit from organizing frequently-shared files into collections. Store commonly-used documents in the extension’s local storage and upload them with a single click. This approach works well for team collaboration when you repeatedly share the same resources.
Design teams use screenshot-and-upload extensions to share UI mockups during code review without saving files locally. Customer support engineers use them to grab and share annotated screenshots directly from support tickets. Content writers use them to quickly drop images into CMS editors by pasting URLs. The pattern scales to nearly any role that involves sharing files frequently.
Publish to Chrome Web Store
Once your extension is working locally, packaging and publishing is straightforward:
- Run
zip -r quick-upload.zip manifest.json popup.html popup.js background.js styles.css icons/from your project root. - Go to the Chrome Web Store Developer Dashboard.
- Pay the one-time $5 developer registration fee if you haven’t already.
- Upload your zip, fill in the store listing details, add at least one screenshot, and submit for review.
- Review typically takes 1-3 business days for new extensions with sensitive permissions.
For personal or team use, you can skip the Web Store entirely by enabling Developer Mode in chrome://extensions/ and clicking “Load unpacked” to load your project directory directly.
Conclusion
Chrome extensions provide a powerful platform for quick file sharing. By using modern APIs like File System Access, Desktop Capture, and chrome.storage, you can create streamlined workflows that eliminate friction from file transfers. The combination of keyboard shortcuts, drag-and-drop support, and a persistent upload history gives users a tool that genuinely replaces multi-step manual workflows with a single gesture. Start with the basic implementation outlined here, then customize based on your specific needs, whether that’s integrating with particular cloud providers, adding compression, or implementing end-to-end encryption for sensitive documents.
Try it: Estimate your monthly spend with our Cost Calculator.
Related Reading
- Best Way to Use Claude Code for Large File Refactoring
- How to Block File Downloads in Chrome Using Group Policy
- Chrome Extension Compress Images Before Upload: Practical Guide
Built by theluckystrike. More at zovo.one
Find the right skill → Browse 155+ skills in our Skill Finder.
Configure permissions → Build your settings with our Permission Configurator.