Display a complete Muṣḥaf page

Put a printed page on screen exactly as the muṣḥaf typesets it, and address every ayah on it. The page is one SVG file that already carries a transparent region per verse; you fetch it, put it in the document, and listen. For text you lay out yourself, see Display Qur'an text; for a single word on the page, this guide gets you the page and Make words clickable takes over.

Platforms
Anything that renders SVG
Time
About 12 minutes

By the end, a page of the Madinah muṣḥaf sits in a container of your choosing, scaled to fit, clicking any verse gives you its reference, and a function goes the other way, from a reference to the page it is printed on.

  1. Fetch the page and put it in the document

    There is no package to install. The pages are static files in the repository, served by raw.githubusercontent.com with open CORS, so a plain fetch works from a browser. Folders are mushafs/<riwayah>/<publisher>/, pages are zero-padded to three digits.

    const RAW = "https://raw.githubusercontent.com/quran-ws/quran-svg/main/mushafs";
    const pad = (n: number) => String(n).padStart(3, "0");
    
    async function mountPage(host: HTMLElement, mushaf: string, page: number) {
      const res = await fetch(`${RAW}/${mushaf}/svg/${pad(page)}.svg`);
      if (!res.ok) throw new Error(String(res.status));
      host.innerHTML = await res.text();
    
      const root = host.querySelector("svg")!;
      root.removeAttribute("width");
      root.removeAttribute("height");
      root.setAttribute("style", "width:100%;height:auto;display:block");
      root.querySelectorAll("#content, #ayah_markers").forEach((g) => {
        (g as SVGGElement).style.pointerEvents = "none";
      });
      return root;
    }
    
    const root = await mountPage(document.querySelector("#page")!, "hafs/kfqc", 42);
    

    Inline (innerHTML, not an img tag) is the mode that gives you the ayah layer. An image element renders the same pixels and gives you nothing to address.

    Five muṣḥafs are vectorised: hafs/kfqc, warsh/kfqc, qalon/kfqc, douri/kfqc and shubah/kfqc. Each is 604 pages. The pointer-events line matters for four of them: in Ḥafṣ the ayah polygons are the last elements in the file and sit on top of the glyphs, but in the other four they come first, so without it a tap on a letter lands on the letter and never reaches the polygon.

  2. Fit the artwork to its ink

    On most pages the declared viewBox is tight around the drawing. On the opening spread — pages 1 and 2 — it is not: page 1 of Ḥafṣ declares -53.3109 -198.4777 345 550 and the drawing fills about a third of that. Measuring the rendered content once and writing the result back makes every page fill its container:

    requestAnimationFrame(() => {
      try {
        const b = (root as SVGGraphicsElement).getBBox();
        if (b.width > 0 && b.height > 0) {
          const p = Math.max(b.width, b.height) * 0.04;
          root.setAttribute("viewBox", `${b.x - p} ${b.y - p} ${b.width + p * 2} ${b.height + p * 2}`);
        }
      } catch {
        /* keep the declared viewBox */
      }
    });
    

    getBBox() needs the element to be laid out, so it waits a frame.

  3. Address every ayah on the page

    Each verse on the page is one path.ayahPolygon, and it carries its identity as attributes. To list what is on the page, read them off:

    const onPage = Array.from(root.querySelectorAll<SVGPathElement>(".ayahPolygon")).map((p) => ({
      surah: Number(p.getAttribute("surah")),
      ayah: Number(p.getAttribute("ayah")),
      el: p,
    }));
    // page 1 of hafs/kfqc → [{ surah: 1, ayah: 1 }, … { surah: 1, ayah: 7 }]
    

    The same list is published beside the page as json/042.json, one entry per ayah with surahNumber, ayahNumber and the polygon — useful when you need to know what is on a page without loading its artwork.

    Keep surah and ayah. The element also carries id="verse-N"; do not parse it — it is numbered globally across all five muṣḥafs, so the same verse has a different id in each.

  4. Turn a tap into a reference

    for (const { surah, ayah, el } of onPage) {
      el.style.cursor = "pointer";
      el.addEventListener("click", () => open(surah, ayah)); // your tafsīr, audio, bookmark…
    }
    
    fill: none silently stops the taps

    The polygons ship as fill-opacity="0". If you hide the layer with .ayahPolygon { fill: none } instead, it looks identical, but under SVG’s default pointer-events: visiblePainted a shape with no fill is not a hit target, so every tap falls through to the page behind. Hide with opacity; keep the fill.

  5. Show the selection

    Raise the polygon’s opacity. Nothing else on the page needs to change.

    const select = (el: SVGPathElement, on: boolean) => {
      el.setAttribute("fill", "#15705D");
      el.setAttribute("fill-opacity", on ? "0.35" : "0");
    };
    

    A polygon is usually several rectangles: an ayah starts and ends mid-line, so its region is a stack of line-strips, and the highlight follows the verse where a bounding box would not. Highlight an ayah covers the same operation across every block.

  6. Go the other way: which page is 2:255 on?

    json/surah.json in each muṣḥaf lists all 114 surahs with the page each one starts on, and the per-page JSON files are tiny — the Ḥafṣ median is 1,679 bytes. A bounded binary search over them settles any reference in at most six requests, with no index to build:

    interface PolyEntry { surahNumber: number; ayahNumber: number; x: number; y: number; polygon: string }
    interface SurahEntry { number: number; pageNumber: number; ayahCount: number; nameEnglish: string }
    
    const surahs: SurahEntry[] = await fetch(`${RAW}/hafs/kfqc/json/surah.json`).then((r) => r.json());
    
    async function pageOf(surah: number, ayah: number) {
      let lo = surahs[surah - 1].pageNumber;
      let hi = surah < 114 ? surahs[surah].pageNumber : 604;
    
      while (lo <= hi) {
        const mid = Math.floor((lo + hi) / 2);
        const entries: PolyEntry[] = await fetch(`${RAW}/hafs/kfqc/json/${pad(mid)}.json`).then((r) => r.json());
    
        if (entries.some((e) => e.surahNumber === surah && e.ayahNumber === ayah)) return mid;
    
        const below = entries.some((e) => e.surahNumber < surah || (e.surahNumber === surah && e.ayahNumber < ayah));
        if (below) lo = mid + 1;
        else hi = mid - 1;
      }
      return null;
    }
    
    await pageOf(2, 255);   // 42
    

    Then mountPage(host, "hafs/kfqc", 42) and select() the polygon whose attributes match.

  7. Get the words of the ayah that was tapped

    The page is glyph outlines. There is no text inside the file, so the tap gives you a reference and nothing more. Quran Text turns the reference into words, and because hafs/kfqc follows the same numbering as the bundled Ḥafṣ edition, the two agree ayah for ayah:

    import { Mushaf } from "quran-text";
    const m = await Mushaf.hafs();
    
    function open(surah, ayah) {
      const a = m.ayah(surah, ayah);
      caption.textContent = a.render({ marks: true, ayahMarks: true });
    }
    

    For warsh/kfqc, load the Warsh edition instead; its numbering matches that muṣḥaf, and Support multiple riwayat explains why the two cannot be mixed.

Try it live

Click any ayah on any of the 604 pages, in five riwayat, against the files fetched live from the repository.

Page 42 is page 42 of one muṣḥaf only

All five muṣḥafs have 604 pages, so page numbers look interchangeable. They are not: each riwayah divides its verses differently and is typeset separately, so page 42 of Ḥafṣ opens at 2:253 and page 42 of Warsh opens at 2:251. Persist the muṣḥaf key beside every page number you store, and re-resolve the page with pageOf() when the reader changes reading.

When this is the wrong block

How this data is made

Pages start as the publisher's own vector edition, so the letter shapes are the printed ones rather than a trace of a scan. The ayah layer is derived from each page's own ayah medallions and audited against the ink: every polygon in a muṣḥaf accounts for exactly one of its ayat, 6,236 in the 604 Ḥafṣ pages.