Use cases›Country Picker
A select menu of every country, the kind of control you'd drop into a sign-up form or shipping address. The dropdown below is rendered here for the demo; the JavaScript that follows is what you'd ship to build the same list from the live API at runtime.
It loads every country, skips any without a two-letter code, sorts them by common name, and falls back gracefully when a flag emoji is missing. Your own API key is filled in below when you're signed in.
<select id="countries"></select>
/* Drop-in styling for the country picker select. */
#countries {
appearance: none;
-webkit-appearance: none;
-moz-appearance: none;
width: 100%;
height: 38px;
margin: 0;
padding: 0 28px 0 8px;
font: inherit;
font-size: 13px;
color: inherit;
cursor: pointer;
outline: none;
border: 1px solid #dddddd;
border-radius: 4px;
background-color: #ffffff;
background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='10' height='6' viewBox='0 0 10 6'><path fill='%23999' d='M0 0l5 6 5-6z'/></svg>");
background-repeat: no-repeat;
background-position: right 4px center;
background-size: 6px;
}
#countries:focus {
border-color: #0a0a0a;
}
// Drop-in country picker for the REST Countries API. Create one for any
// <select>, then call populate() to fill it. Make as many as you need.
// Docs: https://restcountries.com/docs
class RESTCountriesCountryPicker {
constructor(options) {
if (Boolean(options.selector) === false) {
throw new Error('RESTCountriesCountryPicker: a "selector" option is required.');
}
if (Boolean(options.api_key) === false) {
throw new Error('RESTCountriesCountryPicker: an "api_key" option is required.');
}
this.selector = options.selector;
this.api_key = options.api_key;
this.api_url = 'https://api.restcountries.com/countries/v5';
this.paid_plan = options.paid_plan === true;
this.excluded_country_codes = options.excluded_country_codes || [];
this.regions = options.regions || [];
this.subregions = options.subregions || [];
this.languages = options.languages || [];
this.default_country_code = options.default_country_code || null;
this.value_field = options.value_field || 'codes.alpha_2';
// Paid plans can request up to 500 records per call, enough to
// pull every country at once. Free plans cap at 100 per call and
// must page through the full list with offset.
this.page_size = this.paid_plan === true ? 500 : 100;
}
// Builds the request URL for one page: the fields to project and the
// page window. Filtering happens client-side in keep_country.
build_request_url(offset) {
// Request the fields used for display and filtering, plus
// whichever field supplies the option value.
const fields = ['names.common', 'codes.alpha_2', 'flag.emoji'];
if (fields.includes(this.value_field) === false) {
fields.push(this.value_field);
}
if (this.regions.length > 0) {
fields.push('region');
}
if (this.subregions.length > 0) {
fields.push('subregion');
}
if (this.languages.length > 0) {
fields.push('languages');
}
const params = new URLSearchParams({
response_fields: fields.join(','),
limit: String(this.page_size),
offset: String(offset)
});
return `${this.api_url}?${params}`;
}
async load_countries() {
const countries = [];
let offset = 0;
while (true) {
const response = await fetch(this.build_request_url(offset), {
headers: { Authorization: `Bearer ${this.api_key}` }
});
const body = await response.json();
countries.push(...body.data.objects);
// A paid plan returns every country in a single 500-record
// page, so there is nothing left to fetch. Free plans keep
// paging until the API reports no more records.
if (this.paid_plan === true || body.data.meta.more !== true) {
break;
}
offset += this.page_size;
}
return countries;
}
// Resolves a dot-path (e.g. 'codes.alpha_3') against a country,
// returning null when any step along the path is missing.
get_field_value(country, field) {
const value = field.split('.').reduce((current, key) => {
if (current === null || current === undefined) {
return null;
}
return current[key];
}, country);
return value ?? null;
}
// True when value case-insensitively equals one of the candidates.
value_in(value, candidates) {
const needle = (value || '').toLowerCase();
return candidates.some((candidate) => candidate.toLowerCase() === needle);
}
// True when the country lists at least one of the requested
// languages among its official languages.
has_requested_language(country) {
const spoken = (country.languages || []).map((language) => (language.name || '').toLowerCase());
return this.languages.some((language) => spoken.includes(language.toLowerCase()));
}
// Client-side filters applied to every country the API returns:
// drop entries with no alpha-2 code, excluded codes, and anything
// outside the requested regions, subregions, or languages.
keep_country(country) {
const code = country.codes?.alpha_2;
if (Boolean(code) === false) {
return false;
}
if (this.excluded_country_codes.includes(code.toLowerCase()) === true) {
return false;
}
if (this.regions.length > 0 && this.value_in(country.region, this.regions) === false) {
return false;
}
if (this.subregions.length > 0 && this.value_in(country.subregion, this.subregions) === false) {
return false;
}
if (this.languages.length > 0 && this.has_requested_language(country) === false) {
return false;
}
return true;
}
async populate() {
const select = document.querySelector(this.selector);
const countries = await this.load_countries();
countries
.filter((country) => this.keep_country(country))
.sort((a, b) => a.names.common.localeCompare(b.names.common))
.forEach((country) => {
const value = this.get_field_value(country, this.value_field);
if (value === null) {
return;
}
const emoji = country.flag?.emoji ?? '';
const prefix = [emoji, country.codes.alpha_2].filter(Boolean).join(' ');
const option = document.createElement('option');
option.value = String(value).toLowerCase();
option.textContent = `${prefix} - ${country.names.common}`;
select.append(option);
});
// Pre-select a country when a default code was provided.
if (this.default_country_code !== null) {
select.value = this.default_country_code.toLowerCase();
}
// Fall back to the first option when nothing ended up selected,
// e.g. the default code was filtered out of the list.
if (select.selectedIndex === -1 && select.options.length > 0) {
select.selectedIndex = 0;
}
}
}
// Create one per <select> โ instantiate as many pickers as you need.
new RESTCountriesCountryPicker({
selector: '#countries',
api_key: 'YOUR_API_KEY',
paid_plan: false,
excluded_country_codes: ['aq'],
default_country_code: 'us',
value_field: 'codes.alpha_2'
}).populate();