The Chrome team recently announced the new <geolocation> HTML element. It’s a standard HTML button that triggers the browser’s location permission prompt when clicked.
React doesn’t support it yet, but here’s a workaround to try it out anyway.
Creating a <geo-location> custom element
There’s a polyfill available for the <geolocation> element, but it returns early if your browser supports <geolocation>. This puts us in a bind for React, since React doesn’t support the <geolocation> element, but the polyfill just exits early and never creates the <geo-location> custom element if your browser does support it.
My workaround is to copy out the custom element code from the polyfill, minus the early exit.
Here’s my implementation if you’d like to copy and paste:
View full code
// Nothing to do if the element is natively supported
if ('HTMLGeolocationElement' in window) {
return;
}
// Serialize Position for Forms
function serializePosition(pos) {
if (!pos) return null;
return {
latitude: pos.coords.latitude,
longitude: pos.coords.longitude,
};
}
// Custom element
class GeoLocationElement extends HTMLElement {
static get formAssociated() {
return true;
}
static get observedAttributes() {
return ['accuracymode', 'autolocate'];
}
constructor() {
super();
this._internals = this.attachInternals();
this.attachShadow({ mode: 'open' });
this._position = null;
this._error = null;
this._watchId = null;
}
// Attributes & Props
get position() {
return this._position;
}
get error() {
return this._error;
}
get accuracymode() {
return this.getAttribute('accuracymode') || 'approximate';
}
set accuracymode(val) {
this.setAttribute('accuracymode', val);
}
get autolocate() {
return this.hasAttribute('autolocate');
}
set autolocate(val) {
val
? this.setAttribute('autolocate', '')
: this.removeAttribute('autolocate');
}
get watch() {
return this.hasAttribute('watch');
}
// Lifecycle
connectedCallback() {
this.render();
this._handleInlineEvents(); // Bind onlocation="..."
// Setup button listener
this.shadowRoot.querySelector('button').addEventListener('click', (e) => {
// Prevent form submission if the button is inside a form
e.preventDefault();
this.start();
});
if (this.autolocate) this._attemptAutolocate();
}
disconnectedCallback() {
this._stop();
}
attributeChangedCallback(name, oldVal, newVal) {
if (oldVal === newVal) return;
if (name === 'accuracymode') this._updateButtonText();
// If active and settings change, restart
if (this._watchId || this._position) {
this._stop();
if (this.autolocate) this.start();
}
}
// Rendering
render() {
if (this.shadowRoot.innerHTML.trim() !== '') return; // Don't re-render if exists
const style = /* html */ `
<style>
:host { display: inline-block; font-family: system-ui, sans-serif; }
button {
padding: 8px 12px;
cursor: pointer;
background: #f0f0f0;
border: 1px solid #ccc;
border-radius: 4px;
font-size: 0.9em;
display: inline-flex;
align-items: center;
gap: 6px;
}
button:hover { background: #e0e0e0; }
button:active { background: #d0d0d0; }
.icon { width: 12px; height: 12px; background: currentColor; border-radius: 50%; opacity: 0.5; }
.icon.active { color: #2ecc71; opacity: 1; }
.icon.error { color: #e74c3c; opacity: 1; }
</style>
`;
this.shadowRoot.innerHTML = /* html */ `
${style}
<button type="button">
<span class="icon"></span>
<span class="text"></span>
</button>
`;
this._updateButtonText();
}
_updateButtonText() {
const btnText = this.shadowRoot.querySelector('.text');
if (btnText) {
const mode = this.accuracymode === 'precise' ? 'precise ' : '';
btnText.textContent = `Use ${mode}location`;
}
}
_setStatus(status) {
const icon = this.shadowRoot.querySelector('.icon');
icon.className = 'icon'; // reset
if (status === 'success') icon.classList.add('active');
if (status === 'error') icon.classList.add('error');
}
// Logic
_handleInlineEvents() {
const onLocationAttr = this.getAttribute('onlocation');
if (onLocationAttr) {
this.addEventListener('location', (event) => {
try {
new Function('event', onLocationAttr).call(this, event);
} catch (e) {
console.error('Handler error:', e);
}
});
}
}
async _attemptAutolocate() {
if (!navigator.permissions) return;
try {
const res = await navigator.permissions.query({ name: 'geolocation' });
if (res.state === 'granted') this.start();
} catch (e) {
/* ignore */
}
}
start() {
if (!navigator.geolocation) return;
const opts = { enableHighAccuracy: this.accuracymode === 'precise' };
const success = (p) => {
this._position = p;
this._error = null;
this._setStatus('success');
this._updateForm(p);
this._emit();
};
const fail = (e) => {
this._error = e;
this._position = null;
this._setStatus('error');
this._updateForm(null);
this._emit();
};
if (this.watch) {
if (this._watchId) navigator.geolocation.clearWatch(this._watchId);
this._watchId = navigator.geolocation.watchPosition(
success,
fail,
opts
);
} else {
navigator.geolocation.getCurrentPosition(success, fail, opts);
}
}
_stop() {
if (this._watchId) {
navigator.geolocation.clearWatch(this._watchId);
this._watchId = null;
}
}
_updateForm(data) {
this._internals.setFormValue(
data ? JSON.stringify(serializePosition(data)) : null
);
}
_emit() {
this.dispatchEvent(
new Event('location', {
bubbles: true,
})
);
}
}
customElements.define('geo-location', GeoLocationElement);With that, I have a <geo-location> custom element that can be called from a React component.
Using <geo-location> in a React component
import { useState } from 'react';
import './geolocationPolyfill';
export function GeoLocation() {
const [location, setLocation] = useState(null);
const [error, setError] = useState(null);
const handleLocation = (event) => {
if (event.target.position) {
setLocation(event.target.position.coords);
console.log("Location retrieved:", event.target.position);
} else if (event.target.error) {
setError(event.target.error.message);
console.error("Error:", event.target.error.message);
}
}
if (error) {
return <p>{error}</p>
} else if (location) {
return <p>{JSON.stringify(location)}</p>
}
return (
<geo-location
accuracymode='approximate'
onlocation={handleLocation}
></geo-location>
);
}
Conclusion
It was fun to figure out a workaround for using the new <geolocation> element in React. I imagine the attributes will be camelCased if React adds official support, but otherwise this is a pretty good preview of the new element in React.