“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:password@host.com
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://newtab@dev.to
- ✅ 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://newtab@dev.to
-
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 😉).
Top comments (2)
Nice! I read in the comments that it changes the bookmarks and that it may make sense to export all before installing the addon. Would you agree?
Thank you!
Exporting is not required — just toggle the slider in the extension options to revert all bookmarks to their original state: