Home  /  Blog  /  Quran Memorization Tracker

Build a Quran Memorization Tracker App

Tutorial9 min readAugust 2026

This post walks you through building a working Quran memorization tracker app using vanilla JavaScript, the free UmmahAPI Quran API, and localStorage. Users can browse surahs, read ayahs, and mark individual ayahs as memorized. Their progress saves in the browser with no backend needed.

What You Will Build

The app has three screens. First, a surah list that shows all 114 surahs with a progress bar showing how many ayahs are memorized. Second, a surah detail screen that shows each ayah with a checkbox. Third, a summary screen at the top showing total progress across the whole Quran.

Here is what the data model looks like in localStorage:

// localStorage key: "hifz_progress"
// Value is a JSON object like this:
{
  "1": [1, 2, 3, 4, 5, 6, 7],   // Al-Fatiha, all 7 ayahs memorized
  "2": [1, 2, 255],               // Al-Baqarah, 3 ayahs memorized
  "36": [1, 2, 3]                 // Ya-Sin, first 3 ayahs
}

Each key is the surah number as a string. Each value is an array of memorized ayah numbers. This keeps things simple and easy to read back.

Fetching the Surah List

Start by pulling all 114 surahs from UmmahAPI. The /api/quran/surahs endpoint returns the name, English name, total ayah count, and more for each surah. You only need to call this once when the app loads.

async function loadSurahs() {
  const res = await fetch('https://ummahapi.com/api/quran/surahs');
  const data = await res.json();
  return data.surahs; // array of 114 surah objects
}

// Each surah object looks like:
// { number: 1, name: "الفاتحة", englishName: "Al-Fatiha", ayahCount: 7, ... }

Cache this response in a module-level variable. You do not want to fetch 114 surahs every time someone clicks around. Store it once and reuse it.

Building the Surah List Screen

Once you have the surah list, render each one as a card. Read from localStorage to calculate how many ayahs are memorized in each surah, then draw a simple progress bar.

function getProgress() {
  const raw = localStorage.getItem('hifz_progress');
  return raw ? JSON.parse(raw) : {};
}

function renderSurahList(surahs) {
  const progress = getProgress();
  const container = document.getElementById('surah-list');
  container.innerHTML = '';

  surahs.forEach(surah => {
    const memorized = progress[surah.number] ? progress[surah.number].length : 0;
    const total = surah.ayahCount;
    const pct = Math.round((memorized / total) * 100);

    const card = document.createElement('div');
    card.className = 'surah-card';
    card.innerHTML = `
      <div class="surah-meta">
        <span class="surah-num">${surah.number}</span>
        <div>
          <div class="surah-name">${surah.englishName}</div>
          <div class="surah-arabic">${surah.name}</div>
        </div>
        <span class="surah-count">${memorized}/${total}</span>
      </div>
      <div class="progress-bar">
        <div class="progress-fill" style="width:${pct}%"></div>
      </div>
    `;
    card.addEventListener('click', () => openSurah(surah.number));
    container.appendChild(card);
  });
}

The progress bar is just a div with a percentage width. Style it with CSS however you like. Green for complete, a lighter shade for partial progress works well.

Fetching and Displaying a Surah

When a user clicks a surah card, fetch the full surah including all ayahs. Use the /api/quran/surah/:number endpoint. Add ?script=uthmani to get the standard Arabic script.

async function openSurah(surahNumber) {
  const res = await fetch(
    `https://ummahapi.com/api/quran/surah/${surahNumber}?script=uthmani&translations=en`
  );
  const data = await res.json();
  renderSurahDetail(data.surah);
}

function renderSurahDetail(surah) {
  const progress = getProgress();
  const memorized = progress[surah.number] || [];

  const container = document.getElementById('surah-detail');
  container.innerHTML = `<h2>${surah.englishName} &mdash; ${surah.name}</h2>`;

  surah.ayahs.forEach(ayah => {
    const isMemorized = memorized.includes(ayah.number);

    const row = document.createElement('div');
    row.className = `ayah-row ${isMemorized ? 'memorized' : ''}`;
    row.innerHTML = `
      <label class="ayah-check">
        <input type="checkbox" data-surah="${surah.number}" data-ayah="${ayah.number}"
               ${isMemorized ? 'checked' : ''}>
        <span class="ayah-num">${ayah.number}</span>
      </label>
      <div class="ayah-text">
        <div class="arabic">${ayah.arabic}</div>
        <div class="translation">${ayah.translation?.en || ''}</div>
      </div>
    `;
    container.appendChild(row);
  });

  container.querySelectorAll('input[type="checkbox"]').forEach(cb => {
    cb.addEventListener('change', handleToggle);
  });
}

Each ayah row has a checkbox. When checked, the ayah number gets saved to localStorage. When unchecked, it gets removed. The row also shows the Arabic text and English translation side by side.

Tip. Add ?translations=en,ur to the fetch URL if you want both English and Urdu translations at once. The response will include both under the translation object on each ayah.

Saving Progress to localStorage

The toggle function reads the current state, updates the array for that surah, and writes it back. It also re-renders the surah list in the background so the progress bars stay current.

function handleToggle(event) {
  const cb = event.target;
  const surahNum = cb.dataset.surah;
  const ayahNum = parseInt(cb.dataset.ayah, 10);

  const progress = getProgress();

  if (!progress[surahNum]) {
    progress[surahNum] = [];
  }

  if (cb.checked) {
    if (!progress[surahNum].includes(ayahNum)) {
      progress[surahNum].push(ayahNum);
    }
  } else {
    progress[surahNum] = progress[surahNum].filter(n => n !== ayahNum);
  }

  localStorage.setItem('hifz_progress', JSON.stringify(progress));

  // Update the row styling immediately
  const row = cb.closest('.ayah-row');
  row.classList.toggle('memorized', cb.checked);

  // Refresh the overall stats banner
  updateStatsBanner();
}

The updateStatsBanner function reads from localStorage and calculates the total number of memorized ayahs across all surahs. The Quran has 6,236 ayahs total, so you can show a number like "1,204 / 6,236 ayahs memorized".

function updateStatsBanner() {
  const progress = getProgress();
  const totalMemorized = Object.values(progress)
    .reduce((sum, arr) => sum + arr.length, 0);
  const totalAyahs = 6236;
  const pct = ((totalMemorized / totalAyahs) * 100).toFixed(1);

  document.getElementById('stats-total').textContent =
    `${totalMemorized.toLocaleString()} / ${totalAyahs.toLocaleString()} ayahs`;
  document.getElementById('stats-pct').textContent = `${pct}%`;
}

Adding a "Mark Whole Surah" Button

Sometimes a user wants to mark an entire surah as memorized at once. This is a useful shortcut, especially for shorter surahs. Fetch the surah first to get the exact ayah count, then fill the array.

async function markSurahComplete(surahNumber) {
  const res = await fetch(
    `https://ummahapi.com/api/quran/surah/${surahNumber}`
  );
  const data = await res.json();
  const ayahCount = data.surah.ayahCount;

  const progress = getProgress();
  progress[surahNumber] = Array.from(
    { length: ayahCount },
    (_, i) => i + 1
  );

  localStorage.setItem('hifz_progress', JSON.stringify(progress));
  updateStatsBanner();
}

// Example: mark Al-Fatiha (surah 1) as complete
markSurahComplete(1);

You can add a matching "Clear Surah" button that just sets progress[surahNumber] = [] and saves. Both buttons should sit at the top of the surah detail screen.

Showing a Review Ayah

A nice extra feature is a "review" button that shows one of your memorized ayahs at random. This works well as a daily review prompt. Pull a memorized ayah from localStorage and then fetch its text from the API.

async function showRandomMemorizedAyah() {
  const progress = getProgress();
  const allPairs = [];

  Object.entries(progress).forEach(([surah, ayahs]) => {
    ayahs.forEach(ayah => allPairs.push({ surah, ayah }));
  });

  if (allPairs.length === 0) {
    alert('No ayahs memorized yet!');
    return;
  }

  const pick = allPairs[Math.floor(Math.random() * allPairs.length)];

  const res = await fetch(
    `https://ummahapi.com/api/quran/surah/${pick.surah}/ayah/${pick.ayah}?script=uthmani&translations=en`
  );
  const data = await res.json();
  const ayah = data.ayah;

  document.getElementById('review-arabic').textContent = ayah.arabic;
  document.getElementById('review-translation').textContent =
    ayah.translation?.en || '';
  document.getElementById('review-ref').textContent =
    `${ayah.surahEnglishName} ${pick.surah}:${pick.ayah}`;
}

This uses the /api/quran/surah/:s/ayah/:a endpoint to fetch a single ayah. It is faster than loading a full surah just to display one line. Check the Quran API page for the full list of query parameters.

Tip. If you want to play audio for the review ayah, add ?reciter=alafasy to the request. The response includes an audio URL you can pass directly to an HTML <audio> element. Other reciters like sudais and husary are also available.

Exporting and Importing Progress

localStorage is per-browser, so if a user switches devices they lose their data. A simple export-import feature fixes this. Export the JSON to a file and let the user upload it on a new device.

function exportProgress() {
  const data = localStorage.getItem('hifz_progress') || '{}';
  const blob = new Blob([data], { type: 'application/json' });
  const url = URL.createObjectURL(blob);
  const a = document.createElement('a');
  a.href = url;
  a.download = 'hifz_progress.json';
  a.click();
  URL.revokeObjectURL(url);
}

function importProgress(file) {
  const reader = new FileReader();
  reader.onload = (e) => {
    try {
      const parsed = JSON.parse(e.target.result);
      localStorage.setItem('hifz_progress', JSON.stringify(parsed));
      updateStatsBanner();
      alert('Progress imported!');
    } catch {
      alert('Invalid file. Please use a valid hifz_progress.json file.');
    }
  };
  reader.readAsText(file);
}

// Wire up a file input
document.getElementById('import-input')
  .addEventListener('change', e => importProgress(e.target.files[0]));

This requires no server at all. The user gets a JSON file they can keep as a backup or load on another device. It is a small feature that makes the app much more practical.

Project Structure

Here is a simple folder layout for the whole project. No build tools required. Just plain HTML, CSS, and JavaScript.

hifz-tracker/
├── index.html
├── style.css
└── app.js
    ├── loadSurahs()
    ├── renderSurahList(surahs)
    ├── openSurah(surahNumber)
    ├── renderSurahDetail(surah)
    ├── handleToggle(event)
    ├── markSurahComplete(surahNumber)
    ├── showRandomMemorizedAyah()
    ├── exportProgress()
    ├── importProgress(file)
    └── updateStatsBanner()

If you want to scale this into a React app later, the data-fetching logic stays the same. You would just replace the DOM manipulation with state and components. Check out the post on building a Quran reader app with React for how to structure that.

The free tier of UmmahAPI allows 5,000 requests per 15 minutes without an API key, which is more than enough for a personal tracker. If you share the app with others, register for a free key to get unlimited requests.

You can also explore the full API reference in the docs to add extra features like word-by-word breakdown using /api/quran/words/:surah/:ayah or juz-based navigation using /api/quran/juz/:juz.

FAQ

What API do I need to build a Quran memorization tracker app?

You can use the free UmmahAPI at ummahapi.com. It has endpoints for all 114 surahs and individual ayahs with Arabic text and translations. No API key is needed to get started.

How do I save memorization progress without a database?

Use the browser's localStorage API. It stores data as key-value pairs that survive page refreshes. Save a JSON object keyed by surah number, where each value is an array of memorized ayah numbers.

Which UmmahAPI endpoint returns the ayah count per surah?

GET /api/quran/surahs returns all 114 surahs. Each object includes ayahCount, the Arabic name, and the English transliteration. You only need to call this endpoint once and cache the result.

Can I show the Arabic text of each ayah in the tracker?

Yes. Fetch a surah with GET /api/quran/surah/:number?script=uthmani and each ayah object includes the Arabic text in Uthmani script. You can also use indopak or tajweed script options depending on your audience.

Start building with the free Quran API

UmmahAPI gives you Quran, Hadith, Prayer Times, Duas, and more in one free API.

Read the docs