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 (paging through the list on free plans, or pulling all of them in one call on paid plans), 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();