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, 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.
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.