Currency Converter with HTML, CSS & JavaScript

20 DAYS 20 PROJECT CHALLENGE

Day #13

Project Overview

Currency Converter is a beginner-friendly project built using HTML, CSS, and JavaScript. It allows users to enter an amount, select a source currency and a target currency, and convert the amount using current exchange rate data. It fetches exchange rates from an external API, so it can convert almost any pair of currencies. You can choose the currencies you want to convert and just enter the amount. After pressing a button, you will see the converted value. 

This project is useful for beginners who want to practice working with APIs, Fetch API, asynchronous JavaScript, form inputs, calculations, and DOM manipulation while creating a practical and interactive web application

Key Features

  • Currency Amount Input: Users can enter the amount they want to convert. The application will read the input value and use it as the amount for the currency conversion.
  • Multiple Currency Selection: Users can select the target currency from available currency options. This enables the conversion of amounts between different currencies.
  • Real-Time Exchange Rate Data: The application retrieves exchange rate information from an external API. This allows the converter to use current exchange rate data instead of storing fixed conversion values.
  • Automatic Currency Conversion: JavaScript calculates the converted amount using the value entered by the user and the exchange rate obtained from the API. 
  • Swap Currency Option: The user can quickly swap the target and source currencies, thus allowing them to effortlessly exchange the source and target currency values.
  • Dynamic Result Display: The converted amount is updated on the interface after the conversion is completed. Users can clearly see both the original amount and the converted currency value.
  • Exchange Rate Information: The application can display information about the exchange rate used for the conversion, helping users understand how the converted value was calculated.
  • Input Validation: JavaScript can validate the input value entered by the user before the currency conversion takes place, thus preventing erroneous values from being sent to the API.
  • Loading State: The application can display a loading state while waiting for a response from the exchange rate API, thus providing the user with greater feedback as to what the application is currently doing.
  • Error Handling: It can display error messages if the request to the API fails or if the exchange rate information cannot be obtained, thus providing greater feedback if something goes wrong.
  • Responsive Design: The Currency Converter uses a responsive design to ensure that users can comfortably interact with the application on different devices and viewports.

What You'll Learn

  • Understand how to collect values from HTML input fields.
  • Learn how to work with <select> elements in JavaScript.
  • Practice fetching data from an external API.
  • Understand how fetch() is used to make API requests.
  • Learn how async and await handle asynchronous operations.
  • Practice working with JSON data returned by an API.
  • Understand how exchange rate calculations work.
  • Learn how to update HTML elements dynamically using JavaScript.
  • Understand how to validate user input before processing data.
  • Learn how to handle API errors using try and catch.
  • Practice swapping values between two form controls.

HTML Code

HTML creates the structure of the Currency Converter. The application is placed inside the primary container, which contains the project heading, amount input, currency selection fields, conversion controls, and result area. The amount input allows users to enter the value they want to convert and the two <select> elements provide options for choosing the source currency and target currency. These elements allow JavaScript to read the selected currency codes and use them while requesting exchange rate data.

The Convert button starts the conversion process, Swap button can be used to exchange the selected source and target currencies quickly. The result section provides space for displaying the converted amount and exchange rate information, the script.js file is linked at the bottom of the <body> so JavaScript can access the input fields, buttons, result elements, and provide the currency conversion functionality.

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8" />
  <meta name="viewport" content="width=device-width,initial-scale=1" />
  <title>Day 13 — Currency Converter</title>
  <link rel="stylesheet" href="styles.css">
  <!-- Tip: serve this folder over HTTP (Live Server / python -m http.server) — otherwise browser fetch may be blocked. -->
</head>
<body>
  <main class="card" role="main" aria-labelledby="title">
    <header>
      <div>
        <h1 id="title">Day 13: Currency Converter</h1>
        <p class="lead">Tries API first. Falls back to a manual rate & built-in list if the API is unavailable.</p>
      </div>
    </header>

    <section class="converter">
      <div class="row inputs">
        <label class="field">
          Amount
          <input id="amount" type="number" min="0" step="any" value="1" />
        </label>

        <label class="field">
          From
          <select id="fromCurrency" aria-label="From currency"></select>
        </label>

        <label class="field">
          To
          <select id="toCurrency" aria-label="To currency"></select>
        </label>

        <div class="actions">
          <button id="swapBtn" class="btn secondary">Swap</button>
          <button id="convertBtn" class="btn">Convert</button>
        </div>
      </div>

      <div id="result" class="result" aria-live="polite">
        <div class="muted">Result will appear here.</div>
      </div>

      <div id="meta" class="meta muted"></div>
      <div id="error" class="error" role="alert" aria-live="assertive"></div>

      <div class="manual">
        <label class="small">Manual rate (if API fails):</label>
        <input id="manualRate" placeholder="e.g. 0.01234" />
        <button id="applyManual" class="btn secondary">Apply rate</button>
        <button id="testApi" class="btn secondary">Test API</button>
      </div>

      <div class="footer-note small">If you open the file via <code>file://</code> the browser may block requests. Serve with Live Server or <code>python -m http.server</code>.</div>
    </section>
  </main>

  <script src="script.js"></script>
</body>
</html>

CSS Code

CSS controls the appearance and layout of the Currency Converter. The application container is styled with padding, spacing, rounded corners, borders, and shadows to create a clean card-like interface. Flexbox or CSS Grid is used to arrange the amount input and currency selection controls. The currency selectors are styled to make the available options easy to read and select.

The Convert and Swap buttons include interactive styling such as hover effects and cursor changes. The result section is styled separately so that the converted amount is clearly visible to the user. Responsive CSS rules adjust the layout for smaller screens. This allows the input fields and currency controls to remain organized and easy to use on tablets and mobile devices.

:root {
  --bg: #071026;
  --card: #0b1220;
  --accent: #7c3aed;
  --muted: #9aa4b2;
  --white: #e6eef6;
  --danger: #ef4444;
  font-family: Inter, system-ui, -apple-system, 'Segoe UI', Roboto, Arial, sans-serif;
}

html,
body {
  height: 100%;
}

body {
  margin: 0;
  background: #002252;
  color: var(--white);
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 28px;
}

.card {
  width: min(920px, 96%);
  background: linear-gradient(180deg, rgba(255, 255, 255, 0.02), rgba(255, 255, 255, 0.01));
  border-radius: 12px;
  padding: 18px;
  box-shadow: 0 12px 40px rgba(0, 0, 0, 0.6);
  border: 1px solid rgba(255, 255, 255, 0.03);
}

header {
  display: flex;
  gap: 12px;
  align-items: center;
}

.logo {
  width: 48px;
  height: 48px;
  border-radius: 10px;
  display: block;
  object-fit: cover;
}

h1 {
  margin: 0;
  font-size: 18px;
}

.lead {
  margin: 4px 0 12px;
  color: var(--muted);
  font-size: 14px;
}

.converter {
  margin-top: 12px;
}

.row.inputs {
  display: flex;
  gap: 12px;
  flex-wrap: wrap;
  align-items: end;
}

.field {
  display: flex;
  flex-direction: column;
  min-width: 160px;
}

.field input,
.field select {
  padding: 10px 12px;
  border-radius: 8px;
  border: 1px solid rgba(255, 255, 255, 0.04);
  background: #000;
  color: #fff;
  outline: none;
  -webkit-appearance: none;
  appearance: none;
}

.field input::placeholder {
  color: var(--muted);
}

.field input:focus,
.field select:focus {
  box-shadow: 0 0 0 3px rgba(124, 58, 237, 0.06);
  border-color: rgba(124, 58, 237, 0.6);
}

.actions {
  display: flex;
  gap: 8px;
  align-items: center;
}

.btn {
  padding: 10px 14px;
  border-radius: 10px;
  border: 0;
  background: linear-gradient(90deg, var(--accent), #22c1c3);
  color: white;
  font-weight: 600;
  cursor: pointer;
}

.btn.secondary {
  background: transparent;
  border: 1px solid rgba(255, 255, 255, 0.06);
  color: var(--muted);
}

.result {
  margin-top: 16px;
  padding: 12px;
  border-radius: 8px;
  background: rgba(255, 255, 255, 0.02);
  border: 1px solid rgba(255, 255, 255, 0.03);
  min-height: 50px;
  display: flex;
  align-items: center;
  justify-content: space-between;
}

.result .value {
  font-family: ui-monospace, monospace;
  font-size: 18px;
  font-weight: 700;
}

.meta {
  margin-top: 8px;
  color: var(--muted);
  font-size: 13px;
}

.error {
  margin-top: 8px;
  color: var(--danger);
  font-size: 13px;
  min-height: 18px;
}

.manual {
  margin-top: 12px;
  display: flex;
  gap: 8px;
  align-items: center;
}

.manual input {
  width: 160px;
  padding: 8px 10px;
  border-radius: 8px;
  border: 1px solid rgba(255, 255, 255, 0.04);
  background: transparent;
  color: inherit;
}

.small {
  font-size: 13px;
  color: var(--muted);
}

.footer-note {
  margin-top: 12px;
  color: var(--muted);
  font-size: 13px;
}

/* responsive tweaks */
@media (max-width:720px) {
  .row.inputs {
    align-items: stretch;
  }

  .field {
    min-width: calc(50% - 12px);
  }

  .actions {
    width: 100%;
    justify-content: flex-end;
  }

  .manual {
    flex-direction: column;
    align-items: flex-start;
  }
}

Javascript Code

JavaScript handles the main functionality of the Currency Converter. First, it selects the required HTML elements, including the amount input, source currency selector, target currency selector, buttons, and result area. When the user enter details, JavaScript reads the entered amount and selected currency codes and the application then creates an API request to retrieve the required exchange rate data. Since the request may take time to complete, async and await can be used to handle the asynchronous operation.

After receiving the API response, the data is converted into JSON and the relevant exchange rate is extracted. JavaScript then multiplies the entered amount by the exchange rate to calculate the converted value. The converted amount is displayed dynamically inside the result section. Error handling with try and catch helps the application display an appropriate message if the API request fails.

The Swap button exchanges the selected source and target currencies, allowing users to quickly reverse the conversion. Event listeners connect the buttons and form controls to the required functions so users can perform multiple currency conversions without refreshing the webpage.

// script.js — uses ExchangeRate-API v6 (inserted key) with fallback & manual rate
document.addEventListener('DOMContentLoaded', () => {
  // NOTE: keep this root as the key-host portion (no /latest/... appended)
  const API_BASE = 'https://v6.exchangerate-api.com/v6/6b74612b6a28f7f59732e515';

  const amountEl = document.getElementById('amount');
  const fromEl = document.getElementById('fromCurrency');
  const toEl = document.getElementById('toCurrency');
  const convertBtn = document.getElementById('convertBtn');
  const swapBtn = document.getElementById('swapBtn');
  const resultEl = document.getElementById('result');
  const metaEl = document.getElementById('meta');
  const errorEl = document.getElementById('error');
  const manualRateEl = document.getElementById('manualRate');
  const applyManualBtn = document.getElementById('applyManual');
  const testApiBtn = document.getElementById('testApi');

  const SYMBOLS_CACHE_KEY = 'er_symbols_v1';

  const FALLBACK_SYMBOLS = {
    "USD": { description: "United States Dollar" },
    "EUR": { description: "Euro" },
    "INR": { description: "Indian Rupee" },
    "GBP": { description: "British Pound" },
    "JPY": { description: "Japanese Yen" },
    "AUD": { description: "Australian Dollar" },
    "CAD": { description: "Canadian Dollar" },
    "CNY": { description: "Chinese Yuan" }
  };

  function log(...args){ console.log('[CC]', ...args); }
  function fmt(n){ return Number(n).toLocaleString(undefined, { maximumFractionDigits: 6 }); }
  function showError(msg){ errorEl.textContent = msg; }
  function clearError(){ errorEl.textContent = ''; }

  // fetch helper with timeout
  async function fetchWithTimeout(url, opts = {}, ms = 9000) {
    const controller = new AbortController();
    const id = setTimeout(() => controller.abort(), ms);
    try {
      const res = await fetch(url, { ...opts, signal: controller.signal });
      clearTimeout(id);
      return res;
    } catch (err) {
      clearTimeout(id);
      throw err;
    }
  }

  // populate <select>s
  function populateSymbols(symbolsObj){
    fromEl.innerHTML = '';
    toEl.innerHTML = '';

    const placeholderFrom = document.createElement('option');
    placeholderFrom.value = '';
    placeholderFrom.disabled = true;
    placeholderFrom.textContent = 'Select';
    fromEl.appendChild(placeholderFrom);

    const placeholderTo = placeholderFrom.cloneNode(true);
    toEl.appendChild(placeholderTo);

    Object.keys(symbolsObj).sort().forEach(code => {
      const desc = (symbolsObj[code] && (symbolsObj[code].description || symbolsObj[code].name)) || code;
      const o = document.createElement('option');
      o.value = code;
      o.textContent = `${code} — ${desc}`;
      fromEl.appendChild(o);
      toEl.appendChild(o.cloneNode(true));
    });

    // set sensible defaults if available
    if (!fromEl.value) {
      const usdOption = Array.from(fromEl.options).find(o => o.value === 'USD');
      fromEl.value = usdOption ? 'USD' : fromEl.options[1] ? fromEl.options[1].value : '';
    }
    if (!toEl.value) {
      const inrOption = Array.from(toEl.options).find(o => o.value === 'INR');
      toEl.value = inrOption ? 'INR' : toEl.options[1] ? toEl.options[1].value : '';
    }
  }

  // try cache first, then network
  async function loadSymbols() {
    try {
      const cached = localStorage.getItem(SYMBOLS_CACHE_KEY);
      if (cached) {
        const parsed = JSON.parse(cached);
        if (parsed && Object.keys(parsed).length) {
          populateSymbols(parsed);
          // refresh in background
          fetchSymbolsAndCache().catch(e => log('background refresh failed', e));
          return;
        }
      }
    } catch (e) {
      log('cache parse error', e);
    }
    await fetchSymbolsAndCache();
  }

  // ExchangeRate-API v6: /codes -> supported_codes (array of [code, name])
  async function fetchSymbolsAndCache() {
    const url = `${API_BASE}/codes`;
    log('fetching symbols from', url);
    try {
      const res = await fetchWithTimeout(url, {}, 9000);
      log('symbols status', res.status);
      if (!res.ok) throw new Error('HTTP ' + res.status);
      const data = await res.json();
      // Expecting { result: "success", supported_codes: [ ["USD", "United States Dollar"], ... ] }
      const codes = data.supported_codes || data.supportedCodes || data.supportedCodes;
      if (Array.isArray(codes) && codes.length) {
        const obj = {};
        codes.forEach(item => {
          if (Array.isArray(item) && item.length >= 2) obj[item[0]] = { description: item[1] };
          else if (item && item.code) obj[item.code] = { description: item.name || item.description || item.code };
        });
        if (Object.keys(obj).length) {
          localStorage.setItem(SYMBOLS_CACHE_KEY, JSON.stringify(obj));
          populateSymbols(obj);
          clearError();
          log('symbols loaded from API, count=', Object.keys(obj).length);
          return;
        }
      }

      log('symbols response shape unexpected', data);
      populateSymbols(FALLBACK_SYMBOLS);
      showError('Failed to load currencies (API unexpected). Using fallback list.');
    } catch (err) {
      log('fetchSymbolsAndCache error:', err && err.message ? err.message : err);
      populateSymbols(FALLBACK_SYMBOLS);
      const message = (err && err.name === 'AbortError') ? 'Network timeout while loading currencies.' :
                      (err && err.message && err.message.includes('Failed to fetch')) ? 'Network fetch failed (CORS or offline).' :
                      'Unable to load currencies from the network.';
      showError(message + ' Using fallback list.');
    }
  }

  // Convert: use ExchangeRate-API latest endpoint: /latest/{base}
  async function convert(useManual = false, manualRate = null) {
    clearError();
    const amount = Number(amountEl.value);
    const from = fromEl.value;
    const to = toEl.value;
    if (!isFinite(amount) || amount < 0) { showError('Enter a valid amount (>= 0).'); return; }
    if (!from || !to) { showError('Select both currencies.'); return; }

    resultEl.innerHTML = `<div class="muted">Converting…</div>`;
    metaEl.textContent = '';

    // manual override
    if (useManual && manualRate !== null) {
      const converted = amount * Number(manualRate);
      resultEl.innerHTML = `<div class="value">${fmt(amount)} ${from} → ${fmt(converted)} ${to}</div>`;
      metaEl.textContent = `Manual rate used: 1 ${from} = ${fmt(manualRate)} ${to}`;
      return;
    }

    const url = `${API_BASE}/latest/${encodeURIComponent(from)}`;
    log('convert url', url);
    try {
      const res = await fetchWithTimeout(url, {}, 9000);
      log('convert status', res.status);
      if (!res.ok) throw new Error('HTTP ' + res.status);
      const data = await res.json();
      log('convert response', data);
      const rates = data && (data.conversion_rates || data.conversionRates || data.rates);
      const rate = rates && rates[to];
      if ((rate === undefined || rate === null) && rate !== 0) throw new Error('Rate missing in response');
      const converted = amount * rate;
      resultEl.innerHTML = `<div class="value">${fmt(amount)} ${from} → ${fmt(converted)} ${to}</div>`;
      const updated = data && (data.time_last_update_utc || data.time_next_update_utc || data.time_last_update_unix) || '';
      metaEl.textContent = `1 ${from} = ${fmt(rate)} ${to}` + (updated ? ` (Updated: ${updated})` : '');
      clearError();
    } catch (err) {
      log('conversion error', err);
      showError('Conversion failed. You can input a manual rate below and press "Apply rate".');
      resultEl.innerHTML = `<div class="muted">No result</div>`;
    }
  }

  function swapCurrencies() {
    const a = fromEl.value;
    fromEl.value = toEl.value || '';
    toEl.value = a || '';
  }

  // events
  convertBtn.addEventListener('click', (e) => { e.preventDefault(); convert(); });
  swapBtn.addEventListener('click', (e) => { e.preventDefault(); swapCurrencies(); });
  amountEl.addEventListener('keydown', (e) => { if (e.key === 'Enter') convert(); });

  applyManualBtn.addEventListener('click', (e) => {
    e.preventDefault();
    const r = Number(manualRateEl.value);
    if (!isFinite(r) || r <= 0) { showError('Enter a valid manual rate'); return; }
    convert(true, r);
  });

  testApiBtn.addEventListener('click', async (e) => {
    e.preventDefault();
    clearError();
    try {
      const testUrl = `${API_BASE}/pair/USD/INR`;
      const r = await fetchWithTimeout(testUrl, {}, 9000);
      if (!r.ok) throw new Error('HTTP ' + r.status);
      const j = await r.json();
      const sample = (j.conversion_rate !== undefined) ? j.conversion_rate :
                     (j && j.conversion_rates && j.conversion_rates.INR ? j.conversion_rates.INR : 'N/A');
      alert('API test OK — sample: 1 USD = ' + sample);
      clearError();
    } catch (err) {
      log('API test failed', err);
      showError('API appears unreachable from this page (see console).');
    }
  });

  // init
  loadSymbols();
});
Subscribe
Notify of
guest
0 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments

Related Projects

Day 11 : Drum Kit

Play drum sounds when clicking buttons or pressing keys.

Concepts: Keyboard events, Audio API.

Day 15 : Light/Dark Mode Toggle

Switch between light and dark themes with one click (and remember the choice).

Concepts: CSS variables, LocalStorage.

Day 16 : Scroll Progress Bar

Shows a progress bar at the top as the user scrolls down the page.

Concepts: scroll event, math calculations.