Handling Omnibox Input Entered Navigation

Handle Enter in an omnibox session correctly — the three dispositions, resolving free text to a safe URL, suggestion content versus typed text, actions that are not navigations, and restricted targets.

Published September 18, 2026 Updated September 18, 2026 8 min read
Table of Contents

onInputEntered fires when the user presses Enter or clicks a suggestion, with two arguments: the text and a disposition. The text is either the content of the suggestion they chose or whatever they typed if they pressed Enter on the default row — and it is user-controlled input about to become a URL. The disposition says whether they wanted the current tab, a new foreground tab or a new background tab. Getting both right is what makes the keyword feel like part of the browser rather than a toy. This guide is part of omnibox and address bar integration.

What arrives on Enter

The inputs to onInputEnteredThe text argument for a chosen suggestion versus a default-row Enter, and the three disposition values with the user action that produces each.InputProduced byYour handler shouldtext = suggestion contentArrow + Enter, or clickTrust your own content formattext = typed queryEnter on the default rowResolve safely, never navigate rawcurrentTabPlain Entertabs.update the active tabnewForegroundTabAlt+Entertabs.create activenewBackgroundTabMiddle-click a suggestiontabs.create inactive
Two shapes of text and three dispositions — every combination needs a defined outcome.

Step-by-step

1. Honour the disposition

 1async function openFor(url, disposition) {
 2  switch (disposition) {
 3    case "newForegroundTab": return chrome.tabs.create({ url, active: true });
 4    case "newBackgroundTab": return chrome.tabs.create({ url, active: false });
 5    default:                 return chrome.tabs.update({ url });   // currentTab
 6  }
 7}
 8
 9chrome.omnibox.onInputEntered.addListener(async (text, disposition) => {
10  const target = await resolve(text);
11  if (target.kind === "url") return openFor(target.url, disposition);
12  if (target.kind === "action") return runAction(target.action, target.arg);
13});

Execution context: the service worker, registered at the top level. tabs.update with no tab id updates the active tab in the current window, which is where the address bar the user typed into belongs. Ignoring the disposition — always opening a new tab, say — breaks the Alt+Enter habit users bring from every other address-bar interaction.

2. Distinguish chosen suggestions from typed text

Give suggestion content a recognisable shape so the handler knows it came from you.

 1// in onInputChanged
 2suggest(hits.map((h) => ({ content: `doc:${h.id}`, description: /* … */ "" })));
 3
 4// in onInputEntered
 5async function resolve(text) {
 6  if (text.startsWith("doc:")) {
 7    const doc = (await getIndex()).find((d) => d.id === text.slice(4));
 8    if (doc) return { kind: "url", url: doc.url };
 9  }
10  if (text.startsWith("action:")) {
11    const [, action, arg] = text.split(":");
12    return { kind: "action", action, arg };
13  }
14  return resolveTyped(text);
15}

Execution context: the service worker. Using an id rather than a raw URL as content means a stale suggestion resolves against the current index and cannot be used to smuggle an arbitrary URL — a user who edits the text before pressing Enter produces something that does not start with doc:, and falls through to the typed-text path.

3. Resolve typed text safely

The typed-text path handles input you did not generate. It must never become a navigation to whatever the user — or something that pasted into the address bar — wrote.

 1const SEARCH = "https://docs.example.com/search?q=";
 2
 3function resolveTyped(text) {
 4  const q = text.trim();
 5  if (!q) return { kind: "url", url: "https://docs.example.com/" };
 6
 7  // An exact title match goes straight to the page.
 8  const exact = indexByTitle.get(q.toLowerCase());
 9  if (exact) return { kind: "url", url: exact.url };
10
11  // Anything else becomes a search on your own site — never a raw navigation.
12  return { kind: "url", url: SEARCH + encodeURIComponent(q) };
13}

Execution context: the service worker. Encoding the query and prefixing your own search URL means javascript:alert(1) or file:///etc/passwd typed after the keyword becomes a harmless search. If the extension genuinely supports “go to this URL”, validate with new URL() and allow only https: — the same rule as in sanitising untrusted page data in an extension.

4. Support actions that are not navigations

Some omnibox extensions do things rather than go places — create a note, add a task, start a timer. Those still need to acknowledge the user, because the address bar gives no feedback of its own.

1async function runAction(action, arg) {
2  if (action === "note") {
3    await addNote(decodeURIComponent(arg ?? ""));
4    await chrome.action.setBadgeText({ text: "✓" });
5    chrome.alarms.create("clear-badge", { delayInMinutes: 1 });
6  }
7}

Execution context: the service worker. After Enter, the address bar reverts to the current page’s URL; without the badge, the user cannot tell whether the note was saved. Clearing the badge with an alarm rather than a timer is the pattern from alarms vs setTimeout in service workers.

5. Handle targets the extension cannot open

tabs.update to a restricted URL — a chrome:// page, another extension’s page — fails. If your index can contain such targets, catch the failure rather than leaving the user on an unchanged page with no explanation.

1try {
2  await openFor(target.url, disposition);
3} catch (err) {
4  await chrome.tabs.create({ url: chrome.runtime.getURL(`pages/cannot-open.html?u=${encodeURIComponent(target.url)}`) });
5}

Execution context: the service worker. An explanatory extension page is better than silence. Most documentation or link-shortener extensions never meet this, but internal tools that index browser settings pages will.

Resolving what Enter should doText beginning with doc: is resolved against the index, action: runs a command, and anything else is treated as typed input that becomes an exact match or a search on the extension's own site; the disposition then chooses the tab.text starts with doc:our suggestiontext starts with action:our commandanything elsetyped by the userthen apply the dispositionIndex lookup → URLcurrent indexrunAction + badgeno navigationExact title or search URLencoded
No path turns arbitrary typed text into a navigation — the last box is always your own search URL.

Learning from what users choose

Every onInputEntered is a small signal about what the user wanted, and folding it back into ranking makes the keyword noticeably better within a few days of use — without any data leaving the device.

1chrome.omnibox.onInputEntered.addListener(async (text) => {
2  if (!text.startsWith("doc:")) return;
3  const id = text.slice(4);
4  const { picks = {} } = await chrome.storage.local.get("picks");
5  picks[id] = (picks[id] ?? 0) + 1;
6  await chrome.storage.local.set({ picks });
7});

Execution context: the service worker, as a second listener on the same event — both run. The ranking function reads picks as a popularity term, so a page this user opens often rises above an equally good textual match they never choose.

Two refinements keep it well-behaved. Decay the counts over time — halving them monthly — so a page the user needed heavily for one week does not dominate forever. And cap the stored map to the few hundred most-picked ids, so it stays small enough to read on every query. The ranking side is described in providing omnibox suggestions asynchronously.

Alt+Enter on a chosen suggestionThe user arrows to a suggestion and presses Alt+Enter; the worker receives the doc id and newForegroundTab, resolves the id to a URL, opens a new active tab and records the pick.UserAddress barService workerTabsarrow to suggestion, Alt+EnteronInputEntered("doc:alarms", newForegroundTab)resolve id → URLtabs.create({url, active:true})picks["alarms"] += 1
The disposition decides the tab; the id decides the destination; the pick improves the next ranking.

Cross-browser variation

  • Chrome / Edge: onInputEntered(text, disposition) with currentTab, newForegroundTab and newBackgroundTab. tabs.update without a tab id targets the active tab of the current window.
  • Firefox: browser.omnibox.onInputEntered with the same three dispositions. Firefox’s address bar may pass typed text that includes the keyword in some edge cases — trim defensively.
  • Safari: no omnibox. The same resolve function can serve a popup search box’s Enter handler.
  • All three: navigation to restricted URLs fails. Validate typed input and route unknown text to your own search rather than navigating to it.

Verification

  1. Press Enter, Alt+Enter and middle-click on a suggestion; confirm current tab, new foreground tab and new background tab respectively.
  2. Type javascript:alert(1) after the keyword and press Enter; confirm you land on your search page with that string as the query.
  3. Check the recorded picks after a few uses:
1(await chrome.storage.local.get("picks")).picks;
2// { alarms: 3, "service-worker": 1 }

Execution context: the service worker console. The keys should be ids from your index, never raw URLs or typed text.

  1. Run an action: suggestion and confirm the badge acknowledges it.

FAQ

Why does the default row sometimes pass the keyword in text?

It should not in current Chrome, but some engine and version combinations have. Strip a leading keyword and whitespace defensively in resolveTyped.

Can Enter open the extension’s popup instead of navigating?

No — action.openPopup is gesture-restricted and the omnibox Enter does not count in every version. Open an extension page in a tab instead.

Should typed URLs be allowed at all?

Only if the extension’s purpose is navigation, and then only https: URLs after validation. Most search extensions are better off always routing typed text to their own search.

Other UI/UX Patterns & Interactive Components Resources