Device APIs
One import for the camera, location, notifications, files, sharing and the rest of the device. The same call uses the native API in your iOS and Android app and the browser API on the web, and the build installs only what you import.
#Import what you use
Every capability is a named export of @absolutejs/devices. Your code never checks which platform it is on: AbsoluteJS picks the browser, Capacitor or Expo implementation when it builds each target.
import { camera, location, share } from '@absolutejs/devices';bunx absolute mobile syncimport { camera } and import * as devices with devices.camera. It cannot see a capability chosen at runtime, such as devices[name], so name each one you use.#Capabilities
Every method returns a promise. Listeners such as onChange resolve to a function that stops listening.
| Import | Methods | What it does |
|---|---|---|
camera | capability(), permission(), requestPermission(), takePhoto({ direction, transform }) | Take a photo with the front or rear camera, optionally resized. |
photos | capability(), pick({ limit, transform }) | Let the user choose photos. Only the chosen photos are shared with your app. |
documents | capability(op), pick({ accept, limit, maximumBytes }), export({ content, name }), open({ content, name }) | Pick files, save or share a file you made, or open one in the system viewer. Up to 64 MiB by default. |
location | capability(), permission(), requestPermission({ precision }), current(options), watch(listener, options) | Coarse or precise location while the app is open. |
localNotifications | capability(), permission(), requestPermission(), schedule(notification), cancel(ids), pending(), onReceived(listener), onAction(listener) | Notifications your app schedules on the device, now or later. |
pushNotifications | capability(), permission(), requestPermission(), enable(), disable(), onReceived(listener), onAction(listener) | Notifications your server sends. Registration is automatic; see Push notifications. |
share | capability(content), share({ title, text, url, dialogTitle }) | Open the system share sheet. |
clipboard | capability(op), readText(), writeText(value) | Read and write plain text. |
haptics | capability(), impact(style), notification(type), selectionChanged(), vibrate(ms) | Taps and buzzes. Where there is no haptic engine, calls do nothing instead of failing. |
keyboard | capability(), state(), onChange(listener), dismiss() | Whether the on-screen keyboard is open and how tall it is. |
systemBars | capability(op), setAppearance(appearance, bar), setVisible(visible, bar) | Light or dark status-bar icons, and hiding the status or navigation bar. |
platform | capability(), info() | OS, phone or tablet, app version and build, locale, reduced motion and safe-area insets. |
lifecycle | capability(), state(), onChange(listener), onResume(listener), onRestoredOperation(listener) | Active, inactive or in the background, and when the app comes back. |
links | capability(), getLaunchLink(), onOpenLink(listener), openExternal(url) | The link that opened the app, links opened while it runs, and opening a page in the browser. |
network | capability(), status(), onChange(listener) | Online or offline, and Wi-Fi, cellular or ethernet. |
back | capability(), onPress(listener) | The Android Back button and gesture. |
storage | capability(), get(key), set(key, value), remove(key), keys(), clear() | Small preferences as strings, namespaced to your app. |
secureStorage | capability(), get(key), set(key, value), remove(key), keys(), clear() | Secrets in the iOS Keychain or Android Keystore. Never falls back to plain storage. |
#Permissions
Importing a capability or checking it never shows a prompt. permission() reads the current state; requestPermission() asks, so call it from something the user did, like tapping Scan receipt. Taking a photo, reading location and scheduling a notification refuse to run until permission is granted.
import { camera, isDeviceError } from '@absolutejs/devices';
// Call from a button press: requesting permission shows the system prompt.
const scanReceipt = async () => {
const status = await camera.capability();
if (!status.available) return showUploadFallback(status.reason);
const permission = await camera.requestPermission();
if (permission.state !== 'granted') return showCameraHelp(permission);
try {
const photo = await camera.takePhoto({
direction: 'rear',
transform: { width: 1600, height: 1600, quality: 80 }
});
preview.src = photo.webPath;
} catch (error) {
if (isDeviceError(error) && error.code === 'cancelled') return;
throw error;
}
};#Availability and errors
capability() tells you whether a feature works here before you offer it. When it is available it also says how: native in the app, web in the browser, or emulated in tests. When it is not, it gives a reason you can show. Calls that fail throw a DeviceError with one of these codes; isDeviceError(error) narrows it.
#Where each one works
| Capability | Browser | iOS & Android | Expo |
|---|---|---|---|
| camera, photos, documents | |||
| location While the app is open; there is no background location. | |||
| localNotifications In the browser they fire while the page is open. | |||
| pushNotifications Web Push in the browser and installed web app. | With PWA | ||
| share Uses the Web Share API where the browser has it. | |||
| clipboard | |||
| haptics Mapped to vibration patterns in the browser. | |||
| keyboard Estimated from the visual viewport in the browser. | |||
| systemBars The browser can follow the color scheme but not hide bars. The navigation bar exists only on Android. | |||
| platform, lifecycle, links, network, storage | |||
| back | Android | Android | |
| secureStorage Never emulated with plain browser storage. In Expo apps, sign-in credentials are kept in Expo SecureStore by Auth. |
In the app, links.openExternal opens https and http links in the in-app browser; the web version also accepts mailto: and tel:.
#In your components
The capabilities are plain functions, so they work the same in React, Svelte, Vue, Angular, HTML and HTMX pages. Start listeners when a component mounts and call the returned function when it unmounts.
import { useEffect, useState } from 'react';
import { network } from '@absolutejs/devices';
export const OfflineBanner = () => {
const [online, setOnline] = useState(true);
useEffect(() => {
let stop: (() => void | Promise<void>) | undefined;
let active = true;
void network.status().then((status) => setOnline(status.connected));
void network
.onChange((status) => setOnline(status.connected))
.then((unsubscribe) => {
if (active) stop = unsubscribe;
else void unsubscribe();
});
return () => {
active = false;
void stop?.();
};
}, []);
return online ? null : <p role="status">You are offline</p>;
};<script lang="ts">
import { onDestroy, onMount } from 'svelte';
import { lifecycle } from '@absolutejs/devices';
let stop: (() => void | Promise<void>) | undefined;
onMount(async () => {
stop = await lifecycle.onResume(() => refreshInbox());
});
onDestroy(() => {
void stop?.();
});
</script>#Location and storage
Ask for coarse location when a neighbourhood is enough; the system prompt is easier to accept. watch sends positions and errors to one listener until you stop it.
import { location } from '@absolutejs/devices';
const permission = await location.requestPermission({ precision: 'coarse' });
if (permission.state === 'granted') {
const stop = await location.watch(
(event) => {
if (event.type === 'position') moveMarker(event.position);
else showLocationError(event.error);
},
{ accuracy: 'balanced' }
);
// later, when the map closes
await stop();
}storage is for preferences. secureStorage is for anything secret and is only available where the platform can protect it. Sign-in tokens are already kept there for you; see Auth, Sync & HTTP.
import { secureStorage, storage } from '@absolutejs/devices';
await storage.set('theme', 'dark'); // preferences, plain text
await secureStorage.set('pin-hint', hint); // Keychain or Android Keystore#Testing
@absolutejs/devices/testing gives you an in-memory device. Install it, drive it from your test, and check what your code did. Its secure storage is an in-memory stand-in, not real encryption.
import { afterEach, expect, test } from 'bun:test';
import { installDeviceAdapter, network } from '@absolutejs/devices';
import { createTestDeviceAdapter } from '@absolutejs/devices/testing';
let cleanup: (() => void) | undefined;
afterEach(() => cleanup?.());
test('shows the offline banner when the connection drops', async () => {
const device = createTestDeviceAdapter({
platform: { os: 'android', isNative: true }
});
cleanup = installDeviceAdapter(device.adapter);
const seen: boolean[] = [];
await network.onChange((status) => seen.push(status.connected));
device.emitNetwork({ connected: false, connectionType: 'none' });
expect(seen).toEqual([false]);
});#Packages
@absolutejs/devicesThe API your pages import, with the browser, server-rendering and test implementations.
@absolutejs/devices-capacitorThe iOS and Android implementation for Capacitor apps, including the Keychain and Keystore vault.
@absolutejs/devices-expoThe implementation for Expo apps.