· deepdives · 7 min read
Mastering the Contact Picker API: A Step-by-Step Guide
A comprehensive tutorial on the Contact Picker API: feature detection, implementation patterns, TypeScript examples, fallbacks, privacy/security best practices, and testing tips to build a smooth, privacy-first contact selection flow.

Why the Contact Picker API?
The Contact Picker API lets web apps ask the user to choose one or more contacts from their device’s address book and return a small set of properties (like name, email, phone) without exposing the entire address book. This can greatly simplify flows that require user contacts while minimizing permission friction and improving privacy.
Use cases:
- Let users pick a recipient when sharing content or sending invites
- Populate a “refer a friend” form with a selected contact
- Select emergency contact in onboarding
Note: This API is experimental and not yet universally available. Always pair it with progressive enhancement and fallbacks.
Quick compatibility and requirements
- Works only in secure contexts (HTTPS).
- Requires user gesture to invoke (e.g., a click handler).
- Browser support is limited (available in Chromium-based browsers and Chrome for Android behind flags or in supported versions). Check current availability: https://caniuse.com/contact-api
- Feature detection is essential: check for
navigator.contacts
andnavigator.contacts.select
.
References:
- Contact Picker API explainer: https://wicg.github.io/contact-api/
- MDN overview: https://developer.mozilla.org/en-US/docs/Web/API/Contact_Picker_API
- Google Dev article: https://developers.google.com/web/updates/2019/10/contact-picker
Step 1 - Setup and feature detection
Start by detecting support and disabling the contact-picker UI or showing an alternative if unsupported.
function isContactPickerSupported() {
return (
'contacts' in navigator && typeof navigator.contacts.select === 'function'
);
}
if (!isContactPickerSupported()) {
// Hide/disable the "Pick Contact" button and show manual entry fallback.
}
Important: Only call navigator.contacts.select()
in response to an explicit user gesture (click/tap). Calling it outside a user gesture will fail.
Step 2 - Basic implementation (JavaScript)
The basic call takes an array of property names and an options object. Common property names: name
, email
, tel
, address
, icon
.
const props = ['name', 'email', 'tel'];
const options = { multiple: false }; // true to allow selecting more than one
async function pickContact() {
try {
const contacts = await navigator.contacts.select(props, options);
// contacts is an array of objects containing only the requested properties
console.log('Selected contacts:', contacts);
} catch (err) {
// User dismissed the picker or an error occurred
console.error('Contact pick failed or cancelled:', err);
}
}
// example usage in a click handler
document
.getElementById('pick-contact-btn')
.addEventListener('click', async () => {
if (!isContactPickerSupported()) return; // fallback
await pickContact();
});
Returned value example (single contact):
[
{
"name": ["Jane Doe"],
"email": ["jane@example.com"],
"tel": ["+1234567890"]
}
]
Note: Properties are arrays because a contact can have multiple emails or phone numbers.
Step 3 - Practical handling and UI mapping
When you receive the contact, map it into your app model carefully. Example approach:
function mapContactToModel(contact) {
const displayName = (contact.name && contact.name[0]) || '';
const primaryEmail = (contact.email && contact.email[0]) || '';
const primaryPhone = (contact.tel && contact.tel[0]) || '';
return { displayName, primaryEmail, primaryPhone };
}
After mapping, show a confirmation screen so users can verify/confirm the chosen contact before you use or store their information.
Step 4 - TypeScript typing example
type ContactResult = {
name?: string[];
email?: string[];
tel?: string[];
address?: string[];
icon?: ArrayBuffer[] | string[]; // implementation-dependent
};
async function pickContactTS(): Promise<ContactResult[] | null> {
if (!('contacts' in navigator)) return null;
try {
const props = ['name', 'email', 'tel'];
const options = { multiple: false };
const result = (await (navigator as any).contacts.select(
props,
options
)) as ContactResult[];
return result;
} catch (e) {
console.error(e);
return null;
}
}
Note: TypeScript’s DOM lib may not include this API; you’ll often need to extend Navigator
or use any
for navigator.contacts
until type definitions are updated.
Step 5 - Progressive enhancement & fallback strategy
Because support is inconsistent, always provide a fallback experience:
- If feature unsupported, show a simple modal/form to input name/email/phone.
- If the user dismisses the picker or denies the action, show the manual entry fallback.
- Keep UI consistent: label the fallback with the same step/intent so users have a single consistent flow.
Example fallback flow:
- Button: “Choose contact” - if supported, invokes Contact Picker.
- If not supported or cancelled: show a small inline form pre-filled with previously-entered values (if any).
Best practices for UX
- Request the minimum properties you need. Ask only for
tel
if you only need a phone number. - Explain why you’re asking for contact information in the UI (short one-liner) before invoking the picker.
- Let users edit or confirm the returned contact before using it.
- Respect platform styling: use native-looking buttons for calls to the contact picker and maintain consistent flow.
- Provide a graceful spinner or disabled state while awaiting the picker result.
Privacy & Security considerations
- Limit requested properties: only request what you need.
- Do not store contact data unless you must. If stored, minimize retention and encrypt in transit and at rest.
- Make clear the purpose of capturing a contact and display a short privacy note.
- Do not send contact properties to analytics or third-party tools.
- Log only metadata about success/failure (e.g., “picker_accepted”, “picker_cancelled”) without including contact information.
- Validate and sanitize contact data on your server before using it. Treat all input as untrusted.
- Add an explicit consent step if you plan to store or share the contact with others).
Server-side considerations if storing contact data:
- Normalize phone numbers to E.164 using libraries like libphonenumber.
- Deduplicate contacts by hashed or normalized identifiers rather than raw PII.
- Ensure retention policies are documented and accessible to users.
Error handling patterns
- Distinguish between cancellation and other errors. When a user cancels, show the manual entry flow without scaring them with an error dialog.
try {
const contacts = await navigator.contacts.select(props, options);
if (!contacts || contacts.length === 0) {
// treat as cancelled
showManualFallback();
return;
}
// process contacts
} catch (err) {
// Could be a security exception, user gesture requirement, or other issue
console.error(err);
showManualFallback();
}
- Provide helpful, non-technical error messages to the user.
Accessibility (a11y)
- Ensure the trigger button has a meaningful aria-label and that any fallback form is keyboard accessible and screen-reader friendly.
- Use consistent focus management: move focus into a modal or next input after the picker returns or when showing the fallback.
- Provide clear instructions for users who may not have contacts on their device.
Testing tips
- Test on real devices and browsers; emulators may not accurately reflect the native contact picker UI.
- For Chrome on Android you may need a supported version or experimental flags to enable the feature.
- Use feature flags in your app to toggle usage of the Contact Picker for staged rollouts and easier QA.
- Validate behavior when selecting multiple contacts, when a contact has multiple emails/phones, and when the user cancels.
Performance considerations
- The picker is native and won’t drain CPU, but avoid heavy processing right after selection. Process asynchronously to keep UI responsive.
- If you call server APIs after selection, debounce or batch requests if multiple contacts are allowed.
Example: Full flow example (UI pseudocode)
<button id="pick-contact-btn">Choose contact</button>
<div id="fallback-form" hidden>
<!-- Manual entry fields: name, phone, email -->
</div>
document
.getElementById('pick-contact-btn')
.addEventListener('click', async () => {
if (!isContactPickerSupported()) {
document.getElementById('fallback-form').hidden = false;
return;
}
const props = ['name', 'tel'];
const options = { multiple: false };
try {
const contacts = await navigator.contacts.select(props, options);
if (!contacts || contacts.length === 0) {
document.getElementById('fallback-form').hidden = false; // user cancelled
return;
}
const model = mapContactToModel(contacts[0]);
// show confirmation modal populated with model
} catch (e) {
console.warn('Picker error or denied; showing fallback', e);
document.getElementById('fallback-form').hidden = false;
}
});
When not to use the Contact Picker API
- If you need bulk import of contacts - the API only reveals explicit user-selected contacts.
- If you require complete address book access for background sync - the API purposefully avoids broad access.
- If you need cross-platform consistency beyond supported browsers; rely on fallback experiences.
Checklist: Production readiness
- Feature detection and graceful fallback implemented
- Request minimum properties
- UI explains why contacts are requested
- Confirm/allow user to edit the selected contact
- Do not send contact details to analytics
- Encrypt contact data in transit; use secure storage if saving
- Test on supported browsers and devices
- Accessibility and keyboard navigation verified
Useful links and references
- WICG Contact API explainer: https://wicg.github.io/contact-api/
- MDN: Contact Picker API: https://developer.mozilla.org/en-US/docs/Web/API/Contact_Picker_API
- Google Developers overview: https://developers.google.com/web/updates/2019/10/contact-picker
- Can I Use: Contact Picker: https://caniuse.com/contact-api
Summary
The Contact Picker API is a privacy-forward tool that can improve user experience for contact-based flows by letting users explicitly pick contacts. Because support varies, the key to mastering it is graceful feature detection, minimal property requests, clear UI explanations, robust fallbacks, and strong privacy practices. Use the examples above as a blueprint - keep the user in control, and only store contact data when absolutely necessary.