Why I Built TabScribe
After yet another research session where I found myself manually copying 20+ browser tab URLs one by one into my Obsidian notes, I decided enough was enough. I needed a tool that could copy all my tabs at once — in Markdown format.
I searched the Chrome Web Store. The existing options were either:
- Too bloated (OneTab at 2M+ users, but does way more than I need)
- Only supported plain text URL lists (Copy All URLs)
- Abandoned for years (the old TabCopy extension)
So I built TabScribe over a couple of weekends.
What TabScribe Does
One click → all your tabs land in your clipboard in your chosen format:
-
Markdown
[title](url)— perfect for Obsidian, Notion, VS Code - JSON array — great for data processing
- Plain text — with custom delimiters
- HTML unordered list — for web-based notes
Key Features
⚡ Keyboard Shortcut (Alt+Shift+C) — Copy all tabs without opening the popup
💾 Save & Restore Tab Groups — Save your tabs as named groups locally. Restore them in a new window later. Great for context switching.
📤 Export as Files — .md, .txt, .json, or .html with auto timestamps
🎯 Smart Filtering — Auto-excludes chrome://, edge://, about: pages. Optionally exclude pinned tabs.
🔧 Custom Delimiter — Set your own separator for text output (e.g., | for CSV-style)
🌐 12 Languages — English, 中文, 日本語, 한국어, Deutsch, Français, Español, Português, Русский, Bahasa Indonesia, and more
🎨 Dark Mode — Auto-follows system theme
Technical Deep Dive
Tech Stack
Preact + TypeScript + Tailwind CSS + Vite + crxjs
Bundle: ~63KB (gzipped: ~17KB)
Dependencies: 0 external runtime deps
I chose Preact over React deliberately. For a Chrome extension popup, every kilobyte matters. Preact's 3KB footprint vs React's 40KB+ is a no-brainer. Plus, with @preact/preset-vite, the DX is nearly identical.
The Clipboard Challenge (Manifest V3)
The hardest technical problem was clipboard access in Manifest V3's Service Worker context.
In MV3, the background page is replaced by a Service Worker — which has no DOM. The document.execCommand('copy') fallback doesn't work there. Here's my solution:
// Double-fallback clipboard strategy
async function copyToClipboard(text: string, html?: string) {
// Strategy 1: Modern Clipboard API (works in popup + offscreen)
try {
const item = new ClipboardItem({
'text/plain': new Blob([text], { type: 'text/plain' }),
...(html ? { 'text/html': new Blob([html], { type: 'text/html' }) } : {}),
});
await navigator.clipboard.write([item]);
return;
} catch { /* fall through */ }
// Strategy 2: execCommand fallback (popup context only)
if (typeof document !== 'undefined') {
const textarea = document.createElement('textarea');
textarea.value = text;
document.body.appendChild(textarea);
textarea.select();
document.execCommand('copy');
document.body.removeChild(textarea);
return;
}
// Strategy 3: Chrome Offscreen Document (Service Worker context)
await chrome.offscreen.createDocument({ /* ... */ });
// Route clipboard write through the offscreen document
}
i18n Architecture
I used Chrome's built-in chrome.i18n API with 12 locale JSON files. The extension name and description in manifest.json use __MSG_key__ placeholders, so the Chrome Web Store automatically displays the right language.
// _locales/ja/messages.json
{
"extensionName": { "message": "TabScribe" },
"extensionDesc": { "message": "すべてのタブをMarkdown/JSON/HTMLで一括コピー" }
}
Privacy by Design
TabScribe collects zero data. Period.
- No analytics
- No tracking pixels
- No external servers
- No network requests (except extension updates from Chrome)
- Everything stored in
chrome.storage.local
Only 6 permissions requested, each with a clear purpose:
| Permission | Why |
|---|---|
tabs |
Read tab titles & URLs |
clipboardWrite |
Copy formatted text |
storage |
Save preferences & tab groups locally |
commands |
Register keyboard shortcut |
downloads |
Save exported files |
offscreen |
Clipboard access in Service Worker |
Try It
🔗 TabScribe on Chrome Web Store
It's free. No signup. No tracking. Just productivity.
Built with Preact + TypeScript + Tailwind CSS. Source code available on GitHub.
Top comments (0)