Letting Users Delete Omnibox Suggestions
Make extension omnibox suggestions removable: the deletable flag, handling onDeleteSuggestion, what deletion should mean (hide, forget usage, or delete data), undo, and keeping deleted items out of future results.
Table of Contents
Chrome lets users remove entries from the address bar dropdown — hover a history suggestion and click the ✕, or press Shift+Delete. Extension suggestions can opt into the same affordance. Without it, a stale or embarrassing suggestion keeps appearing every time the user types a matching query, and the only way to get rid of it is to find and delete the underlying data somewhere in the extension’s UI. With deletable: true and an onDeleteSuggestion handler, the user removes it right where it bothers them. The interesting part is deciding what “delete” means. This guide covers the API and that decision. It belongs to omnibox and address bar integration.
How deletable suggestions work
Each suggestion passed to suggest() can include deletable: true. Chrome then shows a remove button on that row (and honours Shift+Delete when it is selected). When the user removes it, Chrome fires chrome.omnibox.onDeleteSuggestion with the suggestion’s description text, then removes the row from the current dropdown. Note what you receive: the description, not the content — so your handler must map a description back to the item. The default suggestion cannot be deleted. Deletion is local to the current dropdown until you act on it; the next keystroke calls onInputChanged again, and if you return the item, it reappears.
Step-by-step: deletable suggestions
1. Mark suggestions deletable and keep a lookup
1// sw.js
2let shown = new Map(); // description → item id, for the current dropdown
3
4chrome.omnibox.onInputChanged.addListener(async (text, suggest) => {
5 const items = await search(text);
6 shown = new Map();
7 suggest(items.slice(0, 6).map((it) => {
8 const description = `${highlight(it.title, text)} <url>${escapeXml(it.url)}</url>`;
9 shown.set(description, it.id);
10 return { content: it.url, description, deletable: true };
11 }));
12});
Execution context: the service worker. Because onDeleteSuggestion passes the description, store the exact string you sent. Descriptions must be unique within a dropdown for the lookup to be unambiguous; including the URL usually guarantees that. The map is only needed for the current dropdown, so an in-memory variable is fine — but see step 5 for worker restarts.
2. Handle the deletion
1chrome.omnibox.onDeleteSuggestion.addListener(async (description) => {
2 const id = shown.get(description) ?? (await idFromDescription(description));
3 if (!id) return;
4 await hideFromOmnibox(id);
5});
Execution context: the service worker, with the listener at the top level. The fallback idFromDescription extracts the URL from the <url> element when the in-memory map was lost. The handler should be fast and must not open UI — the user is in the middle of typing.
3. Choose what deletion means
1async function hideFromOmnibox(id) {
2 const { omniboxHidden = [] } = await chrome.storage.local.get("omniboxHidden");
3 if (!omniboxHidden.includes(id)) omniboxHidden.push(id);
4 await chrome.storage.local.set({ omniboxHidden });
5 hiddenSet.add(id);
6}
Execution context: the service worker. In the browser’s own address bar, removing a suggestion removes that history entry — a narrow, expected effect. For an extension, the closest equivalent is usually “don’t suggest this again”, not “delete my saved page”: a user cleaning up the dropdown would be dismayed to find the item gone from their library. Hiding is reversible and loses nothing. If your suggestions are derived from usage (recently opened items), “forget this usage” is another narrow option. Delete the underlying data only if the omnibox is the item’s only interface — and then offer undo.
4. Filter hidden items from future results
1let hiddenSet = new Set();
2chrome.storage.local.get("omniboxHidden").then(({ omniboxHidden = [] }) => { hiddenSet = new Set(omniboxHidden); });
3
4async function search(text) {
5 return (await rank(text)).filter((it) => !hiddenSet.has(it.id));
6}
Execution context: the service worker. Filtering at query time keeps the underlying data intact and makes un-hiding trivial. Load the set at startup so the first keystroke after a worker restart already respects it.
5. Survive worker restarts
The service worker can stop between the moment suggestions are shown and the moment the user deletes one. The new instance will not have the shown map. Make descriptions parseable — for example, always end with <url>…</url> — so the handler can recover the URL and find the item by URL:
1async function idFromDescription(desc) {
2 const m = desc.match(/<url>(.*?)<\/url>/);
3 if (!m) return null;
4 const url = unescapeXml(m[1]);
5 return (await findByUrl(url))?.id ?? null;
6}
Execution context: the service worker. Unescape before looking up, because the description contains the escaped form. See escaping XML in omnibox descriptions.
6. Let users restore hidden items
Add a “Hidden from address bar” list in the options page with a Restore button for each entry. Without it, a misclick is permanent and invisible. Keep the list short by showing titles and URLs, and offer “Restore all”.
7. Offer undo for real deletions
If deletion removes data, keep a short-lived tombstone and show a notification: “Deleted ‘Old project notes’ — Undo”. On Undo, restore from the tombstone. Clear tombstones after a few minutes with an alarm. See handling notification clicks and buttons.
8. Build the restore list in options
1// options.js
2async function renderHidden() {
3 const { omniboxHidden = [] } = await chrome.storage.local.get("omniboxHidden");
4 const items = await chrome.storage.local.get(omniboxHidden.map((id) => `item:${id}`));
5 const list = document.querySelector("#hidden-list");
6 list.replaceChildren(...omniboxHidden.map((id) => {
7 const it = items[`item:${id}`];
8 const li = document.createElement("li");
9 li.textContent = it ? it.title : chrome.i18n.getMessage("deletedItem");
10 const btn = Object.assign(document.createElement("button"), { textContent: chrome.i18n.getMessage("restore") });
11 btn.addEventListener("click", async () => {
12 await chrome.storage.local.set({ omniboxHidden: omniboxHidden.filter((x) => x !== id) });
13 renderHidden();
14 });
15 li.append(btn);
16 return li;
17 }));
18}
Execution context: the options page. Reading the same omniboxHidden key the worker writes keeps both in sync. The worker should listen to storage.onChanged for that key and rebuild hiddenSet, so a restore takes effect on the next keystroke. Items whose underlying data has since been deleted are shown with a placeholder title so the user can still clean them up.
Common mistakes
- Expecting
contentin the handler. You receive the description. - Deleting library data on omnibox removal. Users expect a narrow effect.
- Not filtering on the next keystroke. The item reappears immediately.
- In-memory-only lookup. Fails after a worker restart.
- No way to restore. Misclicks become permanent.
Cross-browser variation
- Chrome / Edge:
deletableandonDeleteSuggestionsupported in current versions. - Firefox:
browser.omniboxsupportsdeletableandonDeleteSuggestionin recent versions; descriptions are plain text, so parse the URL from your own plain-text format. - Safari: no omnibox API.
Verification
- Type a query, remove a suggestion with ✕, then type again and confirm it does not return.
- Restart the browser and confirm it is still hidden.
- Stop the worker between showing and deleting, and confirm deletion still works.
- Restore the item from settings and confirm it reappears.
FAQ
Can the default suggestion be deletable?
No. Only suggestions passed to suggest().
Does Shift+Delete work?
Yes, on a selected deletable suggestion, the same as the remove button.
Should every suggestion be deletable?
Make data-derived suggestions deletable. Built-in commands (help, settings) need not be.
Is a hidden list synced across devices?
Only if you store it in storage.sync. Hiding is usually a local preference, so storage.local is a reasonable default; sync it if users ask for consistent results everywhere.
What if two suggestions have the same description?
The lookup becomes ambiguous. Make descriptions unique by including the URL or another distinguishing detail; if duplicates are genuinely the same item, merge them before suggesting.
Should hiding also remove usage history?
It can. Clearing the item’s usage counts means that, if it is restored later, it starts from a neutral ranking rather than jumping straight back to the top.
Related
- Providing omnibox suggestions asynchronously — the suggestion lifecycle.
- Ranking omnibox suggestions — usage-based ranking.
- Escaping XML in omnibox descriptions — parseable descriptions.
- Omnibox and address bar integration — the parent topic.