REST Countries

Use casesCountry 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();

What you can change

Everything the picker does is driven by the options you pass to new RESTCountriesCountryPicker(...), so this one snippet covers a lot of cases:

selector
Required. CSS selector for the <select> the picker fills. The example targets #countries; point it at any element on your page.
api_key
Required. Your REST Countries API key, sent as a bearer token. When you're signed in it's filled in above; otherwise drop in your own.
paid_plan
true pulls every country in one 500-record request; false pages through the list 100 at a time, the free-plan limit.
excluded_country_codes
Two-letter (alpha-2) codes to leave out, matched case-insensitively. Pass an empty array to render every country.
regions
Array of region names to include, for example ['Europe', 'Americas']. A country must be in one of them. Empty array includes every region.
subregions
Array of subregion names to include, for example ['Northern Europe']. Works on its own or alongside regions. Empty array includes every subregion.
languages
Array of language names; only countries that list one of them as an official language are kept, for example ['Spanish']. Combines with the regions and subregions filters (a country must satisfy them all). Empty array includes every language.
default_country_code
Two-letter (alpha-2) code to pre-select once the list is built, for example 'ca' for Canada. Omit it to leave the first country selected.
value_field
Dot path to the country field used for each option's value, for example 'codes.alpha_2' (the default) or 'codes.alpha_3'. It's added to the request automatically.