Feature Detection

The Feature Detection API allows you to detect specific Cloud Phone client capabilities so your app can offer the best user experience. Use feature detection to build cross-browser web apps that conditionally enable certain features based on supported software and hardware.
Example
PodLP uses feature detection to show a “Download Episode” menu item only on devices that support file downloads using the download
property on an HTMLAnchorElement
.
const fileDownloadSupported = await navigator.hasFeature("FileDownload");
if (fileDownloadSupported) {
downloadLink.setAttribute("download", "podcast.mp3");
downloadLink.classList.remove("hidden");
}
Features
The following features are valid names that can be passed to the navigator.hasFeature
function. Invalid names resolve to false
.
Feature names passed to navigator.hasFeature
are case sensitive!
AudioCapture
True when audio recording using the getUserMedia
is supported.
This is not the same as detecting hardware capabilities or permission status. Catch exceptions throw by getUserMedia
to gracefully handle issues accessing the microphone.
if (await navigator.hasFeature("AudioCapture")) {
return navigator.mediaDevices
.getUserMedia({ audio: true })
.then((mediaStream) => {
const audio = document.querySelector("audio");
audio.srcObject = mediaStream;
})
.catch((err) => {
// Throws NotAllowedError if permission denied
// Throws NotFoundError if no microphone
});
}
AudioPlay
True when audio playback is supported for HTMLAudioElement
.
AudioSeek
True when audio seeking is supported for HTMLMediaElement.currentTime
.
// Show a scrubber when seeking is supported
if (await navigator.hasFeature("AudioSeek")) {
scrubber.classList.remove("hidden");
}
AudioUpload
True when audio uploads are supported using the <input type="file">
element. This specifically only includes the wildcard MIME type audio/*
as the accept
attribute value.
<form id="audioForm" class="hidden">
<label for="audioFile">Upload Audio:</label>
<input type="file" id="audioFile" name="audioFile" accept="audio/*" />
</form>
// Show audio upload button when FileUpload is supported
if (await navigator.hasFeature("FileUpload")) {
document.getElementById("audioForm").classList.remove("hidden");
}
VideoCapture
Same as AudioCapture
but for video. True when video recording using the getUserMedia
is supported.
VideoSeek
Same as AudioSeek
but for video. True when audio seeking is supported using HTMLMediaElement.currentTime
.
VideoUpload
Same as AudioUpload
but for video. True when video uploads are supported using the <input type="file">
element. This specifically only includes the wildcard MIME type video/*
as the accept
attribute value.
EmbeddedTextInput
True when the Cloud Phone client supports embedded text input where the UI displays the Input Method Editor (IME) using a header and footer while maintaining visibility of the <input>
element (right screenshot).
![]() | ![]() |
---|
x-puffin-entersfullscreen
Devices that don’t support embedded input display a fullscreen dialog (left screenshot). This behavior can be fully disabled using the custom x-puffin-entersfullscreen
attribute. Apps disabling the fullscreen IME must intercept KeyEvents
and manually change the input value.

Although Cloud Phone can render emojis, the native IME does not support emoji text input. Create a custom emoji keyboard by mapping keys 0-9 to emojis displayed on screen when the input is focused.
<input type="text" x-puffin-entersfullscreen="off" id="customTextInput" />
// Map emojis to 0-9 keys
const emojiMap = new Map([
["1", "๐"],
["2", "๐"],
["3", "๐"]
// ...
]);
const customInput = document.getElementById("customTextInput");
// Show and hide emoji keyboard
customInput.addEventListener("focus", () => {
showEmojiKeyboard = true;
});
customInput.addEventListener("blur", () => {
showEmojiKeyboard = false;
});
// Append emoji when key is pressed
customInput.addEventListener("keydown", (e) => {
customInput.value += customInput.get(e.key) || "";
});
FileDownload

True when downloads are supported using the download
property on an HTMLAnchorElement
. Download links open a confirmation dialog displaying the file name and size. File name is derived from either the filename
parameter of the Content-Disposition
header, or value of the download
attribute.
<a href="./podcast.mp3" download id="podcastDownloadLink" class="hidden" />
// Show download link when FileDownload is supported
if (await navigator.hasFeature("FileDownload")) {
document.getElementById("podcastDownloadLink").classList.remove("hidden");
}
Without the Content-Disposition
header, both Cloud Phone and Google Chrome block cross-origin <a download>
FileUpload

True when file uploads are supported using the <input type="file">
element. This includes any filename extension or MIME type in the accept
attribute.
Clicking on an <input type="file">
element opens a native fullscreen file picker, like the one pictured above. Users can choose between internal storage (“Phone”) and external storage (“MemoryCard”).
<form id="documentForm" class="hidden">
<label for="document">Upload PDF:</label>
<input type="file" id="document" name="document" accept=".pdf" />
</form>
// Show upload button when FileUpload is supported
if (await navigator.hasFeature("FileUpload")) {
document.getElementById("documentForm").classList.remove("hidden");
}
ImageUpload
Same as AudioUpload
but for images. True when image uploads are supported using the <input type="file">
element. This specifically only includes the wildcard MIME type image/*
as the accept
attribute value.
SmsScheme
True when links using the sms:
URI scheme are supported. The format for URLs of this type is sms:<phone>
, where <phone>
is an optional parameter that specifies a phone number to compose a new SMS message. Valid values can contain the digits 0 through 9, plus (+), hyphen (-), and period (.) characters.
<a href="sms:1-408-555-1212">New SMS Message</a>
TelScheme
True when links using the tel:
URI scheme are supported.
<a href="tel:1-408-555-1212">New SMS Message</a>
Vibrate
True if pulses from vibration hardware are supported using navigator.vibrate
.
navigator.vibrate
always returns true
unless invalid parameters are provided. Use navigator.hasFeature('Vibrate')
to detect if the Vibration API is supported.
Interface
The following TypeScript defines the global hasFeature
function on the navigator
object on both the main thread and a Web Worker context.
declare global {
interface Navigator {
hasFeature(name: string): Promise<boolean>;
}
interface WorkerNavigator {
hasFeature(name: string): Promise<boolean>;
}
}
Availability
The Feature Detection API is available on all Cloud Phone client versions. navigator.hasFeature
only resolves to true
when a given feature is available.
Summary
The Feature Detection API lets you conditionally enable app features based on the capabilities supported by the Cloud Phone client. Use feature detection to build the best user experience without crashing or silently failing.