“I just want my Chrome bookmarks to open in a new tab by default.”
Sounds like a trivial feature, right? As it turns out, implementing this required a deep dive into Chrome APIs, creative hacks, and a journey full of unexpected roadblocks.
In this post, I’ll walk you through how I developed the Chrome extension Open bookmarks in a new tab. What seemed like a two-line tweak — turned out to be a rather tricky challenge, that required a non-trivial and inventive solution.
🧩 The Problem: Why Isn’t This a Built-In Feature?
The goal was simple: prevent the current tab from being replaced when I click on a bookmark. I just wanted Chrome to open bookmarks in a new tab by default.
Searching for “open bookmarks in a new tab” revealed tons of questions on Reddit, SuperUser, and Google Help forums. Clearly, this is something many users want (1, 2, 3).
The usual answers are:
- “Just Ctrl + Click!”
- “Middle-click the bookmark!”
Sure, those work — if you remember. But if you forget, you lose your current page’s context — scroll position, form data, or even unsaved work. That’s frustrating.
Another workaround is to use javascript:
URLs in your bookmarks:
https://dev.to → javascript:window.open("https://dev.to")
This opens the bookmark in a new tab, yes — but at a cost:
❌ You lose the favicon (it defaults to the generic globe icon):
❌
javascript:
links don’t run on internal pages likechrome://extensions
(which is especially annoying if you’re developing Chrome extensions 🤪).
So I started digging into the Chrome Extensions API to find a better way.
🔍 First Attempt: Capture Bookmark Navigation
My initial idea:
- Detect when a tab navigates due to a bookmark click.
- Open that URL in a new tab.
- Use
chrome.tabs.goBack()
to return the current tab to its previous state.
chrome.webNavigation.onCommitted.addListener(details => {
if (details.transitionType === "auto_bookmark") {
chrome.tabs.goBack(details.tabId);
chrome.tabs.create({ url: details.url });
}
});
It kind of worked. But it had a major flaw — the current tab does reload, and any page state is lost. This meant scroll positions, form inputs, or any unsaved data could be wiped out. Not acceptable.
💡 The Breakthrough: Downloads Instead of Navigation
After some thinking, I had an insight.
What if bookmarks triggered a download instead of a navigation?
If the page initiated a download, the current tab wouldn't change. And Chrome Extensions can detect and cancel downloads — avoiding any visual download animation.
So I created a dummy file empty.zip
and added it to the extension. Then I created a bookmark that pointed to that file inside the extension.
chrome.downloads.onCreated.addListener((downloadItem) => {
// capture download of chrome-extension://xxxxx/empty.zip
if (downloadItem.url === chrome.runtime.getURL('/empty.zip')) {
chrome.downloads.cancel(downloadItem.id);
// Now open a new tab with the original URL
}
});
And it worked! 🎉
- ✅ Current tab remained untouched.
- ✅ The download was captured and instantly canceled.
- ✅ No download animation in Chrome UI.
However, two issues emerged:
- The bookmark's favicon changed to the extension’s icon — back to the favicon problem again.
- I needed to know the original URL the user wanted to visit. But now all I had was
/empty.zip
.
So the question became: how can I preserve the original URL, trigger a dummy download, and still keep the favicon?
🕸️ Attempted Tricks: Redirects, Fragments, and More
My next idea: keep the original URL in the bookmark, but redirect it internally using chrome.declarativeNetRequest
.
{
"id": 1,
"priority": 1,
"condition": {
"regexFilter": "????",
"resourceTypes": ["main_frame"]
},
"action": {
"type": "redirect",
"redirect": {
"extensionPath": "/empty.zip"
}
}
}
With this, when a user clicks a bookmark, the navigation would be silently redirected to empty.zip
. The downloadItem
object had both url
and finalUrl
, so I could recover the original URL to open in a new tab:
{
"id": 563,
"url": "https://dev.to/"
"finalUrl": "chrome-extension://xxxxxx/empty.zip",
// ...
}
But the problem? Every navigation to this URL downloads empty.zip
. I had no way to tell which navigations came from bookmarks.
❌ Fragments Don’t Work
I tried appending a hash:
https://dev.to#newtab
But URL fragments don’t reach the network stack — so declarativeNetRequest
ignores them. No help there.
❌ Custom Hostnames? Nope.
I considered using something like:
about:blank?https://dev.to
But again — doing this broke the favicon. Back to the default globe.
I was stuck. I needed a URL that would both initiate a ZIP file download and open in the browser to retrieve the correct favicon. It turned out to be a sort of Schrödinger's URL!
🧙 The Hack That Worked: Username in URL
Then I remembered something from the HTTP spec: URLs can contain a username/password for basic auth.
https://username:[email protected]
This pattern is rarely used today. Most servers ignore the credentials entirely.
What if I use the
username
field to mark the bookmark as special?
For example (password is omitted):
https://[email protected]
- ✅ Chrome displays the original favicon.
- ✅ Most websites ignore the
newtab@
part. - ✅ The username field does get passed to
declarativeNetRequest
.
Bingo! This gave me a unique marker to trigger a redirect to empty.zip
.
🏗️ Final Architecture
Here’s how the extension works end-to-end:
-
Every bookmark URL is rewritten with a username
newtab
:-
https://dev.to
→https://[email protected]
-
Any request with username
newtab
gets redirected toempty.zip
.When
empty.zip
download is triggered, the extension immediately cancels it and opens the original URL in a new tab.Result: The original tab stays untouched. A new tab opens with the target URL. And the favicon is preserved:
💤 The Wakeup Problem
Everything worked beautifully in development. But when testing the production build, a new issue appeared.
After a period of browser inactivity, Chrome suspends background workers in extensions. So when the bookmark is clicked, the extension isn't awake in time to capture the download — and Chrome starts showing a download animation.
To fix this, I added a background alarm
to wake the extension every 30 seconds:
chrome.alarms.create('keepAlive', { periodInMinutes: 0.5 });
chrome.alarms.onAlarm.addListener(() => {
// no-op to keep the background alive
});
This keeps the downloads listener ready and prevents any flicker or download animation.
⚠️ Known Limitations
This hacky-yet-functional solution has some caveats:
-
Internal URLs don’t work.
chrome://
orchrome-extension://
URLs can’t include a username. Demo code:
const url = new URL('chrome://extensions/');
url.username = 'newtab';
console.log(url.href); // → 'chrome://extensions/'
So bookmarks pointing to internal pages still open in the same tab. However, you can still open them in a new tab by Ctrl + Click
.
🎉 Conclusion
What started as a “5-minute side project” turned into a deep exploration of browser behavior, HTTP spec quirks, and extension APIs.
The final solution may look over-engineered — and it kind of is — but it works reliably, preserves user state, and doesn't break UX.
You can try the extension yourself: Open Bookmarks in a New Tab
And if you’re building Chrome extensions yourself, hopefully this journey helps you think outside the box (or at least inside the username
part of the URL 😉).