Four JavaScript APIs That Start to Close the Native Gap
Progressive web apps still live in an awkward middle ground. You can install them to a home screen, but without a few carefully selected APIs, they can never really behave like the native apps they're competing with. Under the hood, a handful of less well-known browser APIs are quietly expanding what a webpage can do — but they've been held back by patchy support and limited awareness (a kind of chicken-and-egg problem in its own right).
Here we look at four of those APIs — the Screen Orientation API, Device Orientation API, Vibration API, and Contact Picker API — and the mechanics that can shift a PWA from pretending to be a project.
Orientation Beyond the Viewport
The screen.orientation object is far more precise than media-query checks. The CSS orientation media feature simply checks whether width is greater than height, while the Screen Orientation API reports the true orientation of the rendering screen itself, which makes it resistant to inconsistencies that can arise when window resizing skews the viewport dimensions.
On this object, two properties are exposed: type, which returns one of four strings — "portrait-primary", "portrait-secondary", "landscape-primary", or "landscape-secondary" — and angle, usually a multiple of 90 degrees from 0 to 360. The type value is informative: a desktop device defaults to landscape, whereas a mobile device in the same orientation reports the true portrait or landscape physical state.
The API also provides an async .lock() method, which takes a type string as an argument, and an .unlock() method to restore the screen to the system's default. An "orientationchange" event fires when the screen rotates.
To put this to use, here's a snippet of HTML that displays values on screen and lets users follow the interaction:
<main>
<p>
Orientation Type: <span class="orientation-type"></span>
<br />
Orientation Angle: <span class="orientation-angle"></span>
</p>
<button type="button" class="lock-button">Lock Screen</button>
<button type="button" class="unlock-button">Unlock Screen</button>
<button type="button" class="fullscreen-button">Go Full Screen</button>
</main>
The JavaScript that pokes the current values into the DOM looks like this:
let currentOrientationType = document.querySelector(".orientation-type");
let currentOrientationAngle = document.querySelector(".orientation-angle");
currentOrientationType.textContent = screen.orientation.type;
currentOrientationAngle.textContent = screen.orientation.angle;
Listening to the orientationchange event updates those values each time the screen rotates:
window.addEventListener("orientationchange", () => {
currentOrientationType.textContent = screen.orientation.type;
currentOrientationAngle.textContent = screen.orientation.angle;
});
Before the screen can be locked, the document must be in full-screen mode. That's where the Fullscreen API's Element.requestFullscreen() method comes in, which we invoke on the root element document.documentElement:
const fullscreenButton = document.querySelector(".fullscreen-button");
fullscreenButton.addEventListener("click", async () => {
// If it is already in full-screen, exit to normal view
if (document.fullscreenElement) {
await document.exitFullscreen();
} else {
await document.documentElement.requestFullscreen();
}
});
From there, screen locking works directly:
const lockButton = document.querySelector(".lock-button");
lockButton.addEventListener("click", async () => {
try {
await screen.orientation.lock(screen.orientation.type);
} catch (error) {
console.error(error);
}
});
The unlock button does the natural opposite:
const unlockButton = document.querySelector(".unlock-button");
unlockButton.addEventListener("click", () => {
screen.orientation.unlock();
});
There is a notable alternative: apps like Instagram and X constrain view to portrait from the manifest.json file's orientation property — not from the JavaScript API — even when the device system orientation is unattached.
Reading Motion in Space
The Device Orientation API lets you access the gyroscope and accelerometer data to position the device in space. It exposes a deviceorientation event that can include the following properties, each one representing a different axis of rotation:
event.alpha— rotation on the Z-axis, from 0 to 360 degrees.event.beta— rotation on the X-axis, from -180 to 180 degrees.event.gamma— rotation on the Y-axis, from -90 to 90 degrees.
Take a 3D cube styled with CSS transforms as a test. Using only one event listener, we can extract the live values and map them onto the cube's transform property:
const currentAlpha = document.querySelector(".currentAlpha");
const currentBeta = document.querySelector(".currentBeta");
const currentGamma = document.querySelector(".currentGamma");
window.addEventListener("deviceorientation", (event) => {
currentAlpha.textContent = event.alpha;
currentBeta.textContent = event.beta;
currentGamma.textContent = event.gamma;
});
The resulting rotation can be simulated on desktop via Chrome DevTools' Sensors Panel for testing.
Applying the rotation itself is mostly a matter of assigning values:
const currentAlpha = document.querySelector(".currentAlpha");
const currentBeta = document.querySelector(".currentBeta");
const currentGamma = document.querySelector(".currentGamma");
const cube = document.querySelector(".cube");
window.addEventListener("deviceorientation", (event) => {
currentAlpha.textContent = event.alpha;
currentBeta.textContent = event.beta;
currentGamma.textContent = event.gamma;
cube.style.transform = `rotateX(${event.beta}deg) rotateY(${event.gamma}deg) rotateZ(${event.alpha}deg)`;
});
Vibration Without the Permission Friction
For letting the user know that a background action has completed, the Vibration API is a lightweight option. It surfaces a single method on navigator.vibrate() that accepts either the number of milliseconds a vibration persists, or an array of numbers that sets up alternating vibration and pause patterns.
navigator.vibrate(200); // vibrate 200ms
navigator.vibrate([200, 100, 200]); // vibrate 200ms, wait 100, and vibrate 200ms.
In practice, a button that accepts a number from the user simply passes that value into the API call on a click event:
const vibrateButton = document.querySelector(".vibrate-button");
const millisecondsInput = document.querySelector("#milliseconds-input");
vibrateButton.addEventListener("click", () => {
navigator.vibrate(millisecondsInput.value);
});
Stopping a vibration isn't special — it's just a call with the value zero. Any current vibration is overridden:
const stopVibrateButton = document.querySelector(".stop-vibrate-button");
stopVibrateButton.addEventListener("click", () => {
navigator.vibrate(0);
});
<main>
<form>
<label for="milliseconds-input">Milliseconds:</label>
<input type="number" id="milliseconds-input" value="0" />
</form>
<button class="vibrate-button">Vibrate</button>
<button class="stop-vibrate-button">Stop</button>
</main>
Server context requirements remain a concern only for certain APIs; as with the rest of this family, the Vibration API isn't broadly supported everywhere, but its coverage makes it one of the least demanding to adopt experimentally.
Accessing Contacts From the Browser
Until recently, pulling device contacts was strictly a native-app capability. The Contact Picker API flips that by providing a contacts.select() method from navigator. Its arguments might look familiar: the properties array holds which data points to request — "name", "address", "email", "tel", and "icon" — and the options object supports a multiple boolean to allow multiple selections at once.
const getContactsButton = document.querySelector(".get-contacts");
const contactList = document.querySelector(".contact-list");
const props = ["name", "tel", "icon"];
const options = {multiple: true};
The async select() call returns contact records, which then come together as DOM list items:
const getContacts = async () => {
try {
const contacts = await navigator.contacts.select(props, options);
} catch (error) {
console.error(error);
}
};
getContactsButton.addEventListener("click", getContacts);
Contact icons take a bit more work, because they come back as blobs that need converting into object URLs first:
const getIcon = (icon) => {
if (icon.length > 0) {
const imageUrl = URL.createObjectURL(icon[0]);
const imageElement = document.createElement("img");
imageElement.src = imageUrl;
return imageElement;
}
};
const appendContacts = (contacts) => {
contacts.forEach(({name, tel, icon}) => {
const contactElement = document.createElement("li");
contactElement.innerText = `${name}: ${tel}`;
contactList.appendChild(contactElement);
const imageElement = getIcon(icon);
contactElement.appendChild(imageElement);
});
};
const getContacts = async () => {
try {
const contacts = await navigator.contacts.select(props, options);
appendContacts(contacts);
} catch (error) {
console.error(error);
}
};
getContactsButton.addEventListener("click", getContacts);
For everything except Chrome Android, Samsung Internet, and native Android web views, this API is still effectively a non-option — a reminder that the greatest caveat with these tools is their sparse portability.
- The API requires permission and a secure context (served over
https://orwss://) to work at all.
Combined, these four tools demonstrate that the web is starting to take seriously what it once dismissed as "device-only." They still carry sharp edges, from partial support to security boundaries — but each represents a stepping stone toward versioned, robust applications that feel at home in an app store. The next time you reach for a dependency to check orientation, trigger a vibration, or call a contact list, know that the platform may already be listening.
Wrapping Up: Four APIs Worth Watching
These four JavaScript APIs stand out because they push the boundaries of what we can build as progressive web apps, yet they remain surprisingly underused. Their limited adoption largely comes down to inconsistent browser support, which is exactly why spreading awareness matters — the more we ask for them, the more likely vendors are to prioritize them.
What makes these APIs particularly compelling is the level of control they offer. We can influence device orientation and screen behavior, tap into hardware features like the vibration motor, and even pull data from other installed apps to enrich our own interfaces. That's a meaningful step toward bridging the gap between the web and native platforms.
Still, there's a chicken-and-egg problem here: low awareness leads to low browser support, which in turn keeps awareness low. So while each of these APIs has real potential, don't expect to use them everywhere right now. Before relying on them in any serious project, check the latest compatibility data on Caniuse or test against your own devices using WebAPI Check. The situation is improving, but it's not uniform yet.
(gg, yk)


