Build a currency picker
Suggest a display currency from a country, let people change it, and apply only the choice they accept.
On this page
Working example
HTML + JavaScriptChoose a display currency
Add a currency preference to your app. Choose France to suggest euros, or pick a currency directly. Use EUR applies the selection to the example's current choice.
The browser download is a complete picker. Use this example prepares its HTML with your public key. It starts with a sample app preference of USD; it never infers a person's country or changes a stored preference.
1. Suggest from a country
const data = await getJson(`/country/${country}`, request.signal);
const place = readCountry(data, country);Country already returns a currency code, name, and symbol. That is enough for this picker. Choosing a country makes one lookup; choosing a currency directly makes none.
Keep the country's suggestion separate from the app's accepted choice. If someone chooses a currency while a request is pending, their edit wins. Missing or unsupported suggestions leave the currency selector available.
2. Apply the person's choice
Replace the sample currency options with the currencies your app supports. The country field is optional, and the Use button names the currency about to be applied.
Applying updates the visible current choice and emits a currencychange event with event.detail.currency. Your application can handle that event. The example keeps its choice in memory and makes no storage writes.
This is a display preference. It does not convert an amount, change a price's billing currency, or establish the person's language or formatting locale. Use their explicit settings for those decisions.
Add precision only when you need it
If your feature needs the usual number of decimal digits, pass the chosen currency code to Currency:
const currency = await getJson(`/currency/${code}`, signal);
return currencySuggestion(currency, code);The browser includes this optional helper; the picker never calls it. The Node and Python downloads demonstrate this metadata variation with one Country call followed by one Currency call. Run them without arguments for France, or add a two-letter country such as JP after the filename.
digits: 0 is a real result, as with the Japanese yen. null means unknown. Server results keep country_name if the metadata request fails; error.stage identifies the failed lookup. Secret keys remain in PARSEAPI_KEY on your server.
The browser gives a suggestion five seconds, makes no automatic retries, and ignores late results after the country changes. The server downloads use the SDK's ordinary timeout and retry defaults.
See the Country reference for its fields and the Currency reference for metadata. For actual amount conversion, use the dated exchange-rate tutorial.
Complete example
One HTML file with the markup, styles, and JavaScript. Replace YOUR_PUBLIC_API_KEY with your public key, then run it on a domain you have added to that key.
View and copy the complete source
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Display currency picker</title>
<style>
:root { color-scheme: light dark; --bg: #fff; --ink: #152029; --muted: #5e6b75; --line: #d9e2e7; --field: #f6f9fa; --accent: #007c91; --button: #c5f6ff; --error: #b42335; }
@media (prefers-color-scheme: dark) { :root:not([data-theme="light"]) { --bg: #0b1015; --ink: #eff7fa; --muted: #94a6b2; --line: #2c3942; --field: #121b23; --accent: #67e8f9; --button: #67e8f9; --error: #fda4af; } }
:root[data-theme="dark"] { color-scheme: dark; --bg: #0b1015; --ink: #eff7fa; --muted: #94a6b2; --line: #2c3942; --field: #121b23; --accent: #67e8f9; --button: #67e8f9; --error: #fda4af; }
:root[data-theme="light"] { color-scheme: light; }
* { box-sizing: border-box; }
body { margin: 0; padding: 28px; color: var(--ink); background: var(--bg); font: 15px/1.5 system-ui, sans-serif; }
main { max-width: 520px; margin: 0 auto; }
h1, p { margin: 0; }
h1 { font-size: 22px; font-weight: 600; letter-spacing: -.025em; }
.current { margin-top: 22px; padding-bottom: 24px; border-bottom: 1px solid var(--line); }
.eyebrow { color: var(--muted); font-size: 11px; font-weight: 650; letter-spacing: .08em; }
#current-code { display: inline-block; margin-top: 7px; color: var(--accent); font-size: 36px; line-height: 1.1; letter-spacing: -.035em; }
#current-name { display: inline-block; margin-left: 12px; font-size: 14px; color: var(--muted); }
form { margin-top: 24px; }
.fields { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: 14px; }
label { display: block; min-width: 0; font-size: 12px; font-weight: 550; }
.optional { color: var(--muted); font-weight: 400; }
select, button { font: inherit; }
select { display: block; width: 100%; min-width: 0; min-height: 46px; margin-top: 7px; padding: 11px 10px; border: 1px solid var(--line); border-radius: 7px; color: var(--ink); background: var(--field); font-size: 14px; }
select:not([multiple]):where(:not([size]), [size="0"], [size="1"]) { -webkit-appearance: none; appearance: none; padding-right: 40px; background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='m4 6 4 4 4-4' fill='none' stroke='%2371717a' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E"); background-repeat: no-repeat; background-position: right 12px center; background-size: 16px; }
#suggestion { min-height: 20px; margin-top: 10px; color: var(--muted); font-size: 13px; }
#suggestion[data-error="true"] { color: var(--error); }
button { min-height: 44px; margin-top: 17px; padding: 10px 16px; border: 0; border-radius: 7px; background: var(--button); color: #07313c; cursor: pointer; font-size: 14px; font-weight: 650; }
button:disabled { opacity: .5; cursor: default; }
#status { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }
:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; }
@media (max-width: 400px) { body { padding: 20px; } .fields { grid-template-columns: 1fr; } }
</style>
</head>
<body>
<main id="currency-picker">
<h1>Display currency</h1>
<div class="current"><p class="eyebrow">CURRENT CHOICE</p><p><span id="current-code">USD</span><span id="current-name">US dollar</span></p></div>
<form id="currency-form">
<div class="fields">
<label for="country">Suggest from country <span class="optional">(optional)</span>
<select id="country" name="country"><option value="">Choose a country</option><option value="FR">France</option><option value="JP">Japan</option><option value="GB">United Kingdom</option><option value="CA">Canada</option><option value="US">United States</option><option value="DE">Germany</option><option value="AU">Australia</option><option value="CH">Switzerland</option></select>
</label>
<label for="currency">Currency
<select id="currency" name="currency" required><option value="USD" selected>USD · US dollar</option><option value="EUR">EUR · Euro</option><option value="JPY">JPY · Japanese yen</option><option value="GBP">GBP · British pound</option><option value="CAD">CAD · Canadian dollar</option><option value="AUD">AUD · Australian dollar</option><option value="CHF">CHF · Swiss franc</option></select>
</label>
</div>
<p id="suggestion" role="status" aria-live="polite"></p>
<button id="apply" type="submit" disabled>Use USD</button>
</form>
<p id="status" role="status" aria-live="polite" aria-atomic="true"></p>
</main>
<script>
// Restrict this public key to the hostnames where you run the picker.
const API_KEY = 'YOUR_PUBLIC_API_KEY';
const picker = document.querySelector('#currency-picker');
const form = document.querySelector('#currency-form');
const countryInput = document.querySelector('#country');
const currencyInput = document.querySelector('#currency');
const apply = document.querySelector('#apply');
const suggestion = document.querySelector('#suggestion');
const status = document.querySelector('#status');
// Your app owns both its supported currencies and the previously chosen value.
const CURRENCIES = { USD: 'US dollar', EUR: 'Euro', JPY: 'Japanese yen', GBP: 'British pound', CAD: 'Canadian dollar', AUD: 'Australian dollar', CHF: 'Swiss franc' };
let chosen = 'USD', requestRevision = 0, choiceRevision = 0, controller;
function showSuggestion(message, error = false) { suggestion.textContent = message; suggestion.dataset.error = String(error); }
function updateAction() { apply.textContent = `Use ${currencyInput.value}`; apply.disabled = !Object.hasOwn(CURRENCIES, currencyInput.value) || currencyInput.value === chosen; }
async function getJson(path, signal) {
if (!API_KEY.startsWith('parse_public_')) throw new Error('Add your public API key to get a country suggestion.');
const url = new URL(path, 'https://api.parseapi.com'); url.searchParams.set('key', API_KEY);
let onAbort;
const aborted = new Promise((_, reject) => {
onAbort = () => reject(new Error('request_aborted'));
signal.addEventListener('abort', onAbort, { once: true });
if (signal.aborted) onAbort();
});
try {
return await Promise.race([aborted, (async () => {
if (signal.aborted) throw new Error('request_aborted');
const response = await fetch(url, { signal, redirect: 'error', cache: 'no-store' });
if (!response.ok) throw new Error('request_failed');
return response.json();
})()]);
} finally { signal.removeEventListener('abort', onAbort); }
}
function readCountry(data, country) {
if (!data || data.country !== country || typeof data.name !== 'string' || !data.name ||
(data.currency != null && (typeof data.currency !== 'string' || !/^[A-Z]{3}$/.test(data.currency)))) throw new Error('unexpected_response');
return data;
}
async function suggestCurrency() {
const revision = ++requestRevision, previousChoice = choiceRevision;
controller?.abort();
const country = countryInput.value;
showSuggestion('');
if (!country) return;
if (!/^[A-Z]{2}$/.test(country)) { showSuggestion('Choose a country from the list.', true); return; }
const request = new AbortController(); controller = request;
let timedOut = false;
const timer = setTimeout(() => { timedOut = true; request.abort(); }, 5000);
showSuggestion('Finding a currency suggestion.');
try {
// example:country
const data = await getJson(`/country/${country}`, request.signal);
const place = readCountry(data, country);
// /example:country
if (revision !== requestRevision || request.signal.aborted) return;
if (place.currency == null) { showSuggestion('No suggestion available. Choose a currency.'); return; }
if (!Object.hasOwn(CURRENCIES, place.currency)) { showSuggestion(`${place.name} uses ${place.currency}, which this sample app does not offer. Choose a supported currency.`); return; }
if (choiceRevision !== previousChoice) { showSuggestion(`Suggested for ${place.name}: ${place.currency}. Your selection is unchanged.`); return; }
currencyInput.value = place.currency;
updateAction();
showSuggestion(`Suggested for ${place.name}. You can choose another currency.`);
} catch (error) {
if (revision !== requestRevision) return;
showSuggestion(timedOut ? 'The suggestion timed out. You can choose a currency.'
: error.message.startsWith('Add your public') ? error.message : 'No suggestion available. You can still choose a currency.', true);
} finally { clearTimeout(timer); }
}
function currencySuggestion(data, code) {
if (!data || data.currency !== code || typeof data.name !== 'string' || !data.name ||
(data.symbol != null && typeof data.symbol !== 'string') ||
(data.digits != null && (!Number.isInteger(data.digits) || data.digits < 0 || data.digits > 4))) throw new Error('unexpected_response');
// example:result
return { currency: data.currency, name: data.name, symbol: data.symbol ?? null, digits: data.digits ?? null };
// /example:result
}
// Optional for apps that need decimal precision. The picker never calls this.
async function currencyDetails(code, signal) {
if (!/^[A-Z]{3}$/.test(code)) throw new Error('Use a three-letter currency code.');
// example:request
const currency = await getJson(`/currency/${code}`, signal);
return currencySuggestion(currency, code);
// /example:request
}
countryInput.addEventListener('change', suggestCurrency);
currencyInput.addEventListener('change', () => {
choiceRevision++; updateAction(); showSuggestion('');
});
form.addEventListener('submit', event => {
event.preventDefault();
const currency = currencyInput.value;
if (!Object.hasOwn(CURRENCIES, currency) || currency === chosen) return;
choiceRevision++; chosen = currency; updateAction();
document.querySelector('#current-code').textContent = currency;
document.querySelector('#current-name').textContent = CURRENCIES[currency];
showSuggestion(''); status.textContent = `Display currency set to ${currency}.`;
// The host application decides how to use or persist this explicit choice.
picker.dispatchEvent(new CustomEvent('currencychange', { bubbles: true, detail: { currency } }));
});
</script>
</body>
</html>
Need a key? Set up this browser example. For every field and parameter, see Country API reference and Currency API reference.