Skip to content

Capabilities · API

Contact Picker API

Published

Limited availabilityNot supported in Chrome (Desktop), Edge (Desktop), Firefox (Desktop), Firefox (Android), Safari (macOS)WICG draft

The Contact Picker API, navigator.contacts, asks the user to pick entries from the device address book in a picker the browser draws, and hands the page only the contacts and the properties (name, email, tel, address, icon) the user agreed to share. There is no bulk read and no persistent grant: every call shows the picker again.

Chrome 80 on Android (Android 6 or later) is the only stable implementation; address and icon arrived in Chrome 84. Desktop Chrome, Edge, and Firefox do not expose navigator.contacts. Safari on iOS 14.5 ships it behind the “Contact Picker API” experimental feature switch under Settings > Safari > Advanced (off by default), and Samsung Internet 14.0 through 21 exposed the interface but failed when the picker opened (BCD api.ContactsManager). MDN lists the API as experimental and outside Baseline.

navigator.contacts.select(properties)
navigator.contacts.select(properties, options)
navigator.contacts.getProperties()

select() returns Promise<sequence<ContactInfo>>, resolving with an empty array when the user cancels. getProperties() returns Promise<sequence<ContactProperty>> naming the properties the device can supply. Both live on a [SecureContext] interface exposed in Window only; select() additionally requires a top-level browsing context and transient user activation, while getProperties() requires neither.

select() takes a list of requested properties and one option; getProperties() takes nothing.

Parameter Type Required Description
properties sequence<ContactProperty> Yes One or more of "name", "email", "tel", "address", "icon". An empty list rejects; a value the browser does not support rejects.
options.multiple boolean No Default false: the picker lets the user choose exactly one contact. true allows several.

Each resolved ContactInfo carries only the members that were requested, every one a sequence because a contact can hold several values:

Member Type Present when
name sequence<DOMString> "name" was requested
email sequence<DOMString> "email" was requested
tel sequence<DOMString> "tel" was requested
address sequence<ContactAddress> "address" was requested; ContactAddress has the PaymentAddress shape (country, addressLine, region, city, postalCode, and so on)
icon sequence<Blob> "icon" was requested

A member the user chose to withhold in the picker is indistinguishable from a member the contact does not have: both come back as an empty sequence.

select() rejects with one of three names; getProperties() rejects with nothing.

Exception Condition
InvalidStateError The caller is not the top-level traversable (an iframe, for example); a picker is already showing for this navigable; or presenting the picker or reading the contacts source failed.
SecurityError The call has no transient user activation.
TypeError properties is empty, or contains a property outside the contacts source’s supported set (in Chrome 80 to 83, "address" and "icon").

Chromium deviates on the last InvalidStateError case: when the Android picker cannot be launched it rejects with TypeError: Unable to open a contact selector instead.

Both examples feature-detect with 'contacts' in navigator && 'ContactsManager' in window, the check Chrome documents, and fall back to a plain form field, because the API is absent on every desktop browser and on iOS Safari without the feature flag.

Picking one recipient’s email with a typed-address fallback

Section titled “Picking one recipient’s email with a typed-address fallback”

Call select() inside the click handler and treat an empty result as a cancel, not an error. When the API is missing, reveal an <input type="email"> instead of hiding the feature.

const pickButton = document.querySelector('#pick-recipient');
const manualField = document.querySelector('#recipient-email');
const supported = 'contacts' in navigator && 'ContactsManager' in window;
pickButton.hidden = !supported;
manualField.hidden = supported;
pickButton.addEventListener('click', async () => {
try {
const [contact] = await navigator.contacts.select(['name', 'email']);
if (!contact) return; // user cancelled
const email = contact.email[0];
if (!email) {
manualField.hidden = false; // contact had no email address
return;
}
manualField.value = email;
} catch (err) {
console.error(`${err.name}: ${err.message}`);
manualField.hidden = false;
}
});

Reading contact.email[0] can yield undefined even after a successful pick, so the manual field stays as the last resort rather than being removed from the DOM.

Trimming the request to what the device can return

Section titled “Trimming the request to what the device can return”

getProperties() tells you whether address or icon are available before you ask for them; requesting an unsupported property rejects the whole call with TypeError. Filter the wish list first, then open a multi-select picker.

async function pickAttendees(wanted = ['name', 'tel', 'icon']) {
if (!('contacts' in navigator && 'ContactsManager' in window)) {
return null; // caller shows its own attendee form
}
const available = await navigator.contacts.getProperties();
const props = wanted.filter((p) => available.includes(p));
if (props.length === 0) return null;
const contacts = await navigator.contacts.select(props, { multiple: true });
return contacts.map((c) => ({
name: c.name?.[0] ?? '',
tel: c.tel?.[0] ?? '',
avatar: c.icon?.[0] ? URL.createObjectURL(c.icon[0]) : null,
}));
}

getProperties() needs no user gesture, so it can run on page load and decide which buttons to render; only the select() call must wait for the click.

Specifications

SpecificationStatus
Contact PickerWICG draft
Contact Picker API: select() methodW3C draft
  • Legend
  • Yes
  • Partial
  • Flag
  • No
  • Unknown
Browser / PlatformSupportVersionsConfidenceSourceNotes
Chrome (Desktop)No—highsource1
Chrome (Android)Yes80highsource—
Edge (Desktop)No—highsource23
Firefox (Desktop)No—highsource4
Firefox (Android)No—highsource56
Safari (macOS)No—highsource7
Safari (iOS)Flag14.5highsource8
Samsung InternetNoremoved in 22.0highsource910
WebView (Android)Yes80highsource11
  1. No Chrome support is recorded in browser-compat-data.
  2. No Edge support is recorded in browser-compat-data.
  3. Derived by browser-compat-data mirroring from Chrome.
  4. Implementation tracking: https://bugzil.la/1756767.
  5. Implementation tracking: https://bugzil.la/1756767.
  6. Derived by browser-compat-data mirroring from Firefox.
  7. No Safari support is recorded in browser-compat-data.
  8. Behind the `Contact Picker API` preference.
  9. Was supported in Samsung Internet from 14.0 until it was removed in 22.0.
  10. This API was exposed but failed upon opening a contact selector.
  11. Derived by browser-compat-data mirroring from Chrome Android.

Source data: /compatibility/contact-picker.json · Global usage: 37 % (StatCounter 2026-05)

Source: spec · MDN · Last verified 2026-10-03 · Confidence: high (computed from sources)