JavaScript Contact Picker Table

PieceWhat it doesField note
navigator.contactsThe gate objectexists only on Chromium Android in secure contexts - feature-detect contacts in navigator before rendering any of this UI
contacts.getProperties()The support checkpromise of what this device can share: name, tel, email, address, icon - validate your request list against it before select
contacts.select(props, opts)The picker callopens the native sheet from a user gesture and resolves ContactInfo[] - only the contacts the user tapped, nothing else
the properties arrayThe field filtername, tel, email, address, icon - request exactly the fields you need; every field is a separate, visible checkbox in the picker and asking reads as intent
{multiple: false}The batch optiondefault single pick for invite flows; multiple: true turns the picker into a checklist for split-the-check selections
ContactInfoThe result recordcarries only the requested fields; name and tel arrive as arrays, addresses as structured ContactAddress objects
icon as BlobThe photo thumbnailrender via URL.createObjectURL and revoke after paint - the blob is session-local and never re-fetchable
no persistent permissionThe privacy modelevery select() opens the picker again: no address-book grant, no enumeration, no background read - the user is the query engine
Reference: the MDN Contact Picker API. Two calls carry the API: getProperties() validates what the device can share, select() opens a native picker and returns ContactInfo records for exactly the contacts the user tapped - the picker UI is the whole permission system.
Bottom line: feature-detect contacts in navigator, validate fields with getProperties(), call select() from a real click, request the minimum properties, and treat the empty resolution as a normal branch. There is no persistent grant to manage - each share is its own visible user decision.
Related tools: the permissions table (the prompt and grant model), the file system access table (the other user-picks-what-to-share API), the getUserMedia table (the other prompt-gated capture), the WebUSB table and Web Bluetooth table (the Chromium capability family).

The Contact Picker API is the platform's answer to a question that used to require full address-book access: how does a web app get one or two contacts - the person you are inviting, splitting a bill with, adding as an emergency contact - without asking for the entire address book? navigator.contacts.select(['name', 'tel'], {multiple: false}) opens a native picker sheet, the user taps specific contacts, and the promise resolves with only those records, only the requested fields. The site never sees the address book, cannot enumerate it, cannot read it in the background - the user is the query engine, and the picker UI is the whole permission system.

Bottom line: two calls carry the API. getProperties() tells you what this device can share (name, tel, email, address, icon) so you can validate your request list before showing UI; select(properties, options) opens the picker from a user gesture and resolves ContactInfo objects carrying exactly the fields you asked for - name and phone numbers arrive as arrays, addresses as ContactAddress objects with street/city/country structure, icons as Blobs you render with createObjectURL. The options bag has one knob: multiple - leave it false for an invite flow, true for a split-the-check checklist. There is no persistent permission: every select() call opens the picker again, which sounds like friction and is actually the design - each share is an explicit, visible, user-owned decision.

The platform reality: Chromium on Android is the whole audience - no iOS Safari, no Firefox, and desktop Chromium exposes nothing (no hardware surface to pick from). Secure context required, feature-detect with 'contacts' in navigator before rendering the button, and keep a manual-entry input as the fallback lane. The mental model to carry: this is a one-way, per-call pipe from the picker to your form - no contacts list to cache, no sync, no change events - and that is why the permission conversation that haunts native address-book apps simply does not exist here. Request the minimum fields, handle the empty selection (the user can cancel or pick nothing useful), and the API does one job cleanly.

How to use

  1. Feature-detect and fall back: if (!('contacts' in navigator)) show a plain input for manual entry - the API exists only in Chromium on Android, so the check gates the whole feature tree, not just the button.
  2. Validate your field list first: const props = await navigator.contacts.getProperties(); then filter your desired fields to props before calling select - requesting an unsupported property rejects or yields empty records, and the check is one await.
  3. Open the picker from a real gesture: inviteBtn.onclick = async () => { const [c] = await navigator.contacts.select(['name', 'tel']); if (c) fillForm(c); } - no click context, no picker; and note the destructure assumes single-select.
  4. Read ContactInfo defensively: c.name and c.tel are arrays (a contact can have several of each) - take c.name[0] and c.tel[0] for a form, iterate for a checklist; addresses come as ContactAddress objects with .streetAddress, .city, .country fields.
  5. Render and release the icon: if you requested 'icon', the photo arrives as a Blob - const url = URL.createObjectURL(c.icon[0]); img.src = url; and URL.revokeObjectURL(url) after paint so the blob does not linger in memory across a long session.

Frequently asked questions

Why does the Contact Picker have no permission prompt or persistent grant - is that a gap or a design?

A design, and the sharpest privacy idea in the file-access family of APIs. Native address-book access is all-or-nothing: grant once and the app can read, sync and upload every contact, forever, invisibly - which is why the big platform permission dialogs read like privacy surrender documents. The Contact Picker inverts the model: the picker UI is the permission system, shown fresh on every select() call, and the grant is the specific contacts the user taps plus nothing else. There is no 'contacts' permission to persist, no change events to listen for, no way to enumerate what was not shared, and no background read - your page holds exactly the records the user handed over, and if it wants more tomorrow, it has to ask through the picker again. For you as a developer this shapes the UX honestly: a one-time invite flow costs one picker visit; a recurring feature (adding team members over weeks) means re-opening the picker each time, which is the correct trade - the user stays the gatekeeper every single time. The design also means there is nothing to revoke in settings: closing the tab deletes the only copy your page had, and the API gives you no storage that outlives your own persistence choices. Compare the flow to the File System Access API's open-file picker: same philosophy - the user picks the resource, the platform mediates the read, the app never holds a master key.

What do ContactInfo records actually look like, and which fields can I request?

The shareable surface is five properties: 'name', 'tel', 'email', 'address', 'icon'. Whatever you pass in select()'s properties array is what comes back - nothing more, even if the underlying contact record is rich. Shape per field: 'name' resolves as an array of display names (a contact can carry several), 'tel' and 'email' are arrays of strings, 'address' is an array of ContactAddress objects with structured fields - streetAddress, city, postcode, region, country, plus an addressing language code - so parsing is field access, not string surgery, and 'icon' is an array of Blobs (thumbnail photos) that you render through URL.createObjectURL. Three defensive habits: always treat every field as an array even in single-select mode (the user picks a contact with two phone numbers and both may arrive); check for absent fields - a user-selected contact may simply have no email, and the field will be missing or empty rather than null-padded; and remember the records are static snapshots - if the user edits the contact after sharing, your copy does not update, because there is no live link back to the address book by design. The properties array is also your privacy budget: each field is a visible checkbox in the picker UI, so asking for name and tel for an invite reads honest, while requesting address and icon for the same invite reads greedy - and users notice.

Which browsers run it, and what should the unsupported experience look like?

Chromium on Android, full stop: Chrome and Edge on Android phones are the entire real audience - iOS Safari has nothing, Firefox has nothing, and desktop Chromium exposes the API surface so incompletely that you must treat desktop as unsupported even when the feature detect might pass. That makes the fallback path not an edge case but half your traffic: keep a manual-entry form (name and phone inputs) as the default experience, and treat the picker as a progressive enhancement that replaces typing when it exists. Detection is one line - 'contacts' in navigator - but pair it with platform judgment for the honest check: on desktop, hide the pick-from-contacts button entirely rather than showing a control that errors at click time. Secure context is required, so a plain-HTTP intranet deployment silently loses the API too. One UX note for the fallback: manual entry and picker output should converge on the same in-memory shape (normalize both to your ContactInfo-ish object with name and tel fields) so the rest of your form code never learns which path produced the data - that single adapter keeps the Chromium-only enhancement from leaking conditionals through your codebase.

What are the practical failure modes, and how do I build around them?

Five, in the order they bite. No user gesture: select() called from script context (a timer, a fetch handler) fails to open the picker - wire it to real clicks, full stop. Empty resolution: the user opens the picker and backs out, or taps nothing and confirms - the promise resolves to an empty array, which is a normal branch, not an error; render a gentle return to the form, never a red toast. Over-requesting: asking for a property the device does not support (getProperties() is your oracle) can reject the call or yield records missing that field - validate first and degrade the request list. Icon lifecycle: the icon Blob is only valid in this session, and object URLs leak if you never revoke them - one revoke after paint per icon, or a cleanup pass on form submit. And scope confusion: records do not update, cannot be re-fetched by id, and there is no 'recently shared' cache - if your flow needs the same contact twice (draft then confirm), hold the object you already have, because the API will not get it back for you; a second select() means a second user decision. None of these are deep - the API is small - but each one is the difference between a picker that feels native and one that feels like it half-works.

Related tools