Back FoxCode

The <geolocation> element

Chrome 144 ships a declarative HTML element for location access. Instead of a script firing a permission prompt, the user clicks a browser-controlled button. The conversion numbers from the origin trial are hard to ignore.

·7 min read

The Geolocation JS API has had the same UX problem since 2008 and nobody fixed it. navigator.geolocation.getCurrentPosition() fires a browser permission prompt the instant a script calls it - which is usually within seconds of page load, before the user has done anything that signals they want location features. They click "Block." Permission denied, permanently, unless they find the lock icon in the address bar and dig through settings.

Chrome 144 ships <geolocation>, an HTML element that changes when and how location access is requested.

The actual problem with getCurrentPosition()

The API isn't technically broken. The problem is timing and framing. The browser shows a permission dialog the moment a script calls getCurrentPosition() - regardless of what the user was doing or whether they'd expressed any intent to use location features. Most sites call it on page load or component mount.

Once the user clicks "Block", the recovery path is brutal:

  1. Notice the permission icon in the address bar
  2. Click it
  3. Find the location toggle
  4. Re-enable it
  5. Reload the page

Nobody does this. The location feature is dead for that user on that site, permanently.

What <geolocation> actually does

The element is a browser-rendered button. The browser controls what it looks like and what it does. When the user clicks it, the browser handles the permission request, the data retrieval, and the error states. The site listens for a location event:

<geolocation
  onlocation="handleLocation(event)"
  accuracymode="precise">
</geolocation>
function handleLocation(event) {
  if (event.target.position) {
    const { latitude, longitude } = event.target.position.coords
    // do something with coords
  } else if (event.target.error) {
    console.error(event.target.error.message)
  }
}

The browser renders the element as a styled button. The user clicking it is a user gesture - semantically different from a script running. The browser enforces that distinction at the platform level. Scripts cannot programmatically trigger the permission prompt.

Three behaviors that differ from getCurrentPosition():

Permission blocked - recovery is contextual. If the user previously blocked the site, clicking <geolocation> shows UI to re-enable it in place - no settings navigation required. The user is at the exact moment in the flow where they need location; the recovery prompt is relevant.

Permission granted - acts as a refresh. If permission is already granted, clicking the element immediately fetches new coordinates without re-prompting. No stale position from page load.

No surprise prompts. The element can only request permission in response to a user click. No script can call it early or on a timer.

Origin trial numbers

Before shipping, the concept ran as an origin trial (Chrome 126-143) under the name <permission>. Three companies published production data:

CompanyMetricResult
ZoomLocation capture errors-46.9%
Immobiliare.itSuccessful geolocation flows+20%
ZapImóveisPermission recovery success rate+54.4%

Users who had previously blocked location were recovering that permission at a 54.4% higher rate. The recovery UI - shown at the moment the user is actively trying to use a location feature - is far more effective than asking them to go find browser settings.

Attributes

Four attributes control behavior:

autolocate - if permission is already granted, the element attempts to retrieve location on load, without waiting for a click. Useful when you're confident the user wants their location (e.g., on a map page they explicitly navigated to).

accuracymode - "precise" or "approximate". Maps to enableHighAccuracy in the JS API. Approximate uses cell tower and WiFi data; precise uses GPS when available, which is slower and drains battery.

watch - continuous updates, equivalent to watchPosition(). Fires the location event each time the position changes.

<!-- One-time high-accuracy request -->
<geolocation accuracymode="precise" onlocation="onLocation(event)"></geolocation>

<!-- Continuous tracking -->
<geolocation watch onlocation="onMove(event)"></geolocation>

<!-- Auto-fetch if already permitted, else wait for click -->
<geolocation autolocate onlocation="onLocation(event)"></geolocation>

Styling constraints

The element is browser-rendered. The browser enforces visual constraints to prevent deceptive UIs:

  • Contrast: button text must meet a 3:1 minimum contrast ratio
  • Opacity: must be 1 - can't make it transparent to overlay on other elements
  • Size bounds: minimum and maximum width, height, and font size are enforced
  • Transforms: limited to 2D translations and proportional scaling - can't rotate or flip it
  • Negative margins: disabled

These constraints eliminate a specific clickjacking attack: overlaying the element transparently over something else, so a click on "Play video" actually triggers a location permission request. The styling enforcement makes that attack surface disappear at the platform level.

You can still theme it. The element supports :granted for state-aware styling:

geolocation {
  background: #1e1e26;
  border: 1px solid #2a2a36;
  color: #c8cdd6;
  border-radius: 8px;
  padding: 8px 16px;
}

geolocation:granted {
  color: #4ade80;
}

Progressive enhancement

<geolocation> is Chrome 144+ only. For every other browser, the fallback is clean because unknown HTML elements render their children without breaking:

<geolocation onlocation="handleLocation(event)">
  <!-- visible only in browsers that don't support <geolocation> -->
  <button onclick="navigator.geolocation.getCurrentPosition(handleLocation)">
    Use my location
  </button>
</geolocation>

In Chrome 144+, the <geolocation> element renders as a browser button and the child <button> is hidden. In every other browser, the <geolocation> tag is invisible (unknown elements have no default display) and the fallback button shows.

Feature detection:

const supportsGeoElement = 'HTMLGeolocationElement' in window

There's also a polyfill on npm that swaps unsupported instances with a custom element (<geo-location>) backed by the standard JS API.

Interactive demo
JS APIgetCurrentPosition()

Script triggers the prompt immediately. Fires on mount in most implementations.

HTML element<geolocation>

Not supported in this browser. Chrome 144+ required.

Browser support:Not supported - showing fallback

In a Nuxt/Vue context

<geolocation> works in Vue templates since Vue passes through unknown elements as-is. One thing to handle: Vue doesn't know the position and error properties exist on the element, so access them via a template ref rather than v-model or binding:

<script setup>
const geoEl = useTemplateRef('geoEl')
const coords = ref(null)
const geoError = ref(null)

function onLocation() {
  if (geoEl.value?.position) {
    coords.value = geoEl.value.position.coords
  } else {
    geoError.value = geoEl.value?.error?.message
  }
}
</script>

<template>
  <geolocation
    ref="geoEl"
    @location="onLocation"
    accuracymode="precise"
  />
</template>
Add this to nuxt.config.ts to suppress the "unknown component" warning at build time without affecting anything else:
vue: {
  compilerOptions: {
    isCustomElement: tag => tag === 'geolocation'
  }
}

For SSR: the element is a browser-only control and will only render on the client. Wrap it in <ClientOnly> if you're hitting hydration mismatches, or guard it with import.meta.client.

The <usermedia> companion

Chrome 144 also launched an origin trial for <usermedia> - the same concept applied to camera and microphone access. Same user-initiated model, same declarative approach, same styling constraints. The pattern is consistent: permissions that require demonstrated user intent get their own HTML element instead of a JS API that sites call whenever they feel like it.


The JS Geolocation API isn't going away but the origin trial data is hard to argue with: when the user initiates the permission instead of a script doing it on their behalf, they actually grant it more often and recover from blocks more often. The fix was always behavioral, not technical.

Continue Reading