# Contact Picker API

> navigator.contacts.select() opens a browser-drawn picker and returns only the contacts and properties the user chose. Members, exceptions, support, fallbacks.

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.

## Syntax

```js
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.

## Parameters

`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.

## Exceptions

`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.

:::observed
In Chrome on Android, `navigator.contacts.select(['name'])` run from a `setTimeout` callback rejects with `SecurityError: A user gesture is required to call this method`; the same call from an iframe rejects with `InvalidStateError: The contacts API can only be used in the top frame`, and a second call while the sheet is open rejects with `InvalidStateError: Contacts Picker is already in use.` The picker's confirm button reads **Done** in the English Android UI. The three strings are thrown from Chromium's [`contacts_manager.cc`](https://chromium.googlesource.com/chromium/src/+/main/third_party/blink/renderer/modules/contacts_picker/contacts_manager.cc) (chromium.googlesource.com).
:::

## Examples

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

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.

```js
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

`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.

```js
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.

## See also

- [Web Share API](/reference/capabilities/web-share/), the other OS sheet a PWA can open from a gesture
- [Payment Request API](/reference/capabilities/payment-request/), whose `PaymentAddress` shape `ContactAddress` reuses
- [Contact Picker API: select() method](https://w3c.github.io/contact-picker/#contacts-manager-select) (w3.org)
- [A contact picker for the web](https://developer.chrome.com/docs/capabilities/web-apis/contact-picker) (developer.chrome.com)
- [Chrome Platform Status: Contacts API](https://chromestatus.com/feature/6511327140904960) (chromestatus.com)