Use markers, frames and ornaments

Your page is typeset from Quran Text and it looks like a web page. This is how to dress it as a muṣḥaf: a banner above the surah, a roundel after each verse, a border around the page — ornaments traced from printed editions or drawn from a font’s own end-of-ayah glyph, recolourable, with a transparent slot where your text goes.

Platforms
Anywhere SVG renders
Time
About 12 minutes

Two lineages, two licences

The catalogue has 71 assets in two lineages, and the lineage decides whether you may ship the file.

lineageWhat it isCountlicenseShip it?
scanSurah headers, ayah markers and page frames traced from eight printed muṣḥafs24 — 8 of eachCC-BY-NC-SA-4.0 · status: "provisional"Provisional. The designs belong to their publishers and permission is being settled per publisher
fontAyah markers built from the end-of-ayah glyph (U+06DD) of 26 Arabic font families, in their weights — 20 designs4740 × OFL-1.1 · confirmed; 7 × unverified · pendingYes for the 40 confirmed. Read license.status, not the lineage
Nothing is on npm yet

There is no release and no package. The files this page uses are the copies this site serves for demonstration. Every entry in assets-index.json carries its own license block: id, status and the source it was traced from, so you can read the position per asset rather than take it on trust. It is a departure from the default licences a repository carries (Repositories and licensing, rule 1.1), and a deliberate one.

What you get

Three kinds of ornament: a surah header, an ayah marker and a page frame. There is no Qur’anic text in any of these files. The text is yours, from Quran Text; the ornament is a picture frame for it. If you want the whole printed page, text and ornament together, that is Quran SVG, and it needs none of this.

Matching an asset to a printing

Decoration is decoration. Any of these ornaments will dress any edition — a Warsh header above a Ḥafṣ page is a design choice, not an error, and nothing in the data stops you. The style key records the printing a scan asset was traced from, so use the table when you want the page to look like the muṣḥaf it came from. Ḥafṣ has three printings here, and the keys are not the ones the other blocks use.

Asset stylePrintingQuran Text edition key
mushaf-hafs-madinah-mumtazaḤafṣ, Madinah, mumtāzahafs
mushaf-hafs-madinah-kabirḤafṣ, Madinah, large — its frame does not tile
mushaf-hafs-adiḤafṣ, al-ʿAdī
mushaf-shubahShuʿbahshubah
mushaf-warshWarshwarsh
mushaf-qalonQālūnqalun
mushaf-dourial-Dūrīduri
mushaf-sousial-Sūsīsusi

Three keys are spelled differently on the two sides and there is no plain hafs style, so a join needs the table above in your code. Font markers carry riwaya: null — they were never tied to a printing at all.

Pick the variant

Each asset ships as separate drawings, not filters over one file. Scan assets have three; font markers have two.

VariantWhat it isGroups insideQālūn header
colorthe full-colour drawing, one group per printed inkslot, c1cN, line26.1 KB, six inks
monoa single-colour silhouette drawn as currentColorslot, ink12.3 KB
linelinework only, constant-width strokes, currentColor — scan assets onlyslot, line10.4 KB

How many c groups a colour file has depends on the source: the Qālūn header has six, the Ḥafṣ Madinah mumtāza header ten, the Alkalami font marker five. Read them from the file, or from the asset’s palette in the catalogue.

Put a surah header on your page

This is the whole integration, adapted from the code the demo on this site runs.

  1. Find the asset in the catalogue

    assets-index.json is one entry per asset. Look it up by type and style, and take the file name from variants.

    const catalog = await (await fetch("/demo/assets-index.json")).json();
    
    const header = catalog.assets.find((a) => a.type === "surah-headers" && a.style === "mushaf-qalon");
    header.variants.color;      // "assets-surah-headers-mushaf-qalon-color.svg"
    header.slots[0];            // { role: "surah-name", x: 180.8571, y: 8.5714, w: 501.4286, h: 83.1429, cx: …, cy: … }
    header.license;             // { id: "CC-BY-NC-SA-4.0", status: "provisional", … }
    
  2. Inline the SVG

    An <img src> renders the ornament but seals it: no CSS reaches inside, nothing can be positioned over the slot. Fetch the markup and put it in the DOM.

    async function mountOrnament(host: HTMLElement, file: string) {
      host.innerHTML = await (await fetch(`/demo/${file}`)).text();
      const svg = host.querySelector("svg")!;
      svg.style.width = "100%";   // the files carry a viewBox and no width/height; height is always 100
      return svg;
    }
    
    const svg = await mountOrnament(banner, header.variants.color);
    

    The root element of that file, trimmed to what you will use:

    <svg viewBox="0 0 863.429 100" data-asset="surah-header" data-lineage="scan"
         data-mushaf="qalon" data-style="mushaf-qalon" data-variant="color" data-symmetry="4"
         data-slot="180.8571 8.5714 501.4286 83.1429">
    
  3. Convert the slot to percentages

    data-slot — and slots[0] in the catalogue — is x y w h in viewBox units. Never type the numbers into your code: the Qālūn header keeps 58% of its width for the name, the Ḥafṣ Madinah mumtāza header 40%.

    function slotBox(svg: SVGSVGElement) {
      const [, , vbW, vbH] = svg.getAttribute("viewBox")!.split(/\s+/).map(Number);
      const [x, y, w, h] = svg.getAttribute("data-slot")!.split(/\s+/).map(Number);
      return {
        left: `${(x / vbW) * 100}%`,
        top: `${(y / vbH) * 100}%`,
        width: `${(w / vbW) * 100}%`,
        height: `${(h / vbH) * 100}%`,
      };
    }
    
    slotBox(svg);
    // { left: "20.95%", top: "8.57%", width: "58.07%", height: "83.14%" }   ← the Qālūn header
    
    Slot units are neither pixels nor percent

    Treat 180.8571 as pixels and the surah name lands 180 px from the left of a 300 px banner — off the artwork, with no error. Divide by the viewBox first, every time.

  4. Place the surah name over the slot

    The container is position: relative, the ornament fills it, and the name — from Quran Text, in its own font — sits absolutely inside the slot.

    const name = document.createElement("div");
    name.dir = "rtl";
    name.lang = "ar";
    name.textContent = m.surah(2).nameAr;       // from Quran Text — never typed in
    Object.assign(name.style, {
      position: "absolute",
      display: "grid",
      placeItems: "center",
      fontFamily: `"${m.font.family}"`,
      ...slotBox(svg),
    });
    banner.style.position = "relative";
    banner.append(name);
    

    The header slot is sized for a calligraphic surah name. Nothing enforces that; longer text overflows the ornament.

  5. Do the same for the ayah marker

    The marker contract is the same. Its slot has the role ayah-number and is sized for one to three digits. The font markers are the settled set — pick one whose licence is confirmed:

    const marker = catalog.assets.find(
      (a) => a.type === "ayah-markers" && a.lineage === "font" && a.license.status === "confirmed",
    );
    // e.g. ayah-markers/font-001-regular — Alkalami's U+06DD, OFL-1.1
    // viewBox "0 0 99.456 100", data-slot "32.7218 40.5624 34.1679 19.1082"
    
    const roundel = await mountOrnament(afterAyah, marker.variants.color);
    const n = document.createElement("div");
    n.textContent = String(ayah.number);        // the edition's own number — 255 in Ḥafṣ, 253 in Warsh
    Object.assign(n.style, { position: "absolute", display: "grid", placeItems: "center", ...slotBox(roundel) });
    afterAyah.style.position = "relative";
    afterAyah.append(n);
    

    That number is the edition’s own count, read from the loaded muṣḥaf — the second reason the asset key and the edition key travel together.

    Font markers carry data-slot but no slot group

    A scan asset has a transparent <g class="slot" data-part="slot"> you can style; a font marker’s color.svg has only c1c5, with the slot given by the data-slot attribute alone. Position over data-slot as above and both lineages behave the same; a .slot { fill } rule only ever affects scan assets.

  6. Recolour by group

    Colours are presentation attributes, so any CSS rule wins. Every drawable group carries data-part, and a theme does not need to know the muṣḥaf:

    const palette: Record<string, string> = { c2: "#15705D", c3: "#D6AD64" };
    
    svg.querySelectorAll<SVGGElement>("[data-part]").forEach((g) => {
      const part = g.getAttribute("data-part")!;
      if (part === "slot") return;                    // leave the slot transparent
      if (part === "line") { g.setAttribute("stroke", "#222"); return; }   // stroked, not filled
      const printed = g.getAttribute("fill");          // keep it, so "as printed" can come back
      g.setAttribute("fill", palette[part] ?? printed!);
    });
    

    c1 is very often the paper — #ffffff in every scan asset, #fff8e7 in the Alkalami marker. Map it to a dark colour and the ornament turns inside out. data-symmetry="4" on a header means one quadrant is drawn and placed four times with <use>, so one fill change recolours all four corners.

Tile a page border

A frame’s color.svg has the aspect of the page it was scanned from — the Warsh frame is 0 0 68.317 100 — and stretching it to your page distorts the corner motifs. Seven of the eight frames carry a slices block in the catalogue instead, and three slice files, all in the frame’s own units:

"slices": {
  "corner": { "w": 9.4557, "h": 6.4871 },
  "repeat": { "h": 6.1572, "v": 6.2122 },
  "corner_mode": "mirror",
  "files": {
    "corner": "assets-mushaf-warsh-slice-corner.svg",
    "edge-h": "assets-mushaf-warsh-slice-edge-h.svg",
    "edge-v": "assets-mushaf-warsh-slice-edge-v.svg"
  }
}

Each slice file repeats the same facts on its root — viewBox, data-repeat, and data-frame-viewbox="0 0 68.3168 100", the frame it was cut from. The corner is drawn once and mirrored into the other three positions (corner_mode is "mirror" for all seven at this commit); the units repeat along the runs.

/** How many units fit a run, and how much each must stretch so the run ends at the corner. */
function run(total: number, unit: number) {
  const count = Math.max(1, Math.round(total / unit));
  return { count, step: total / count };
}

const { corner, repeat } = frame.slices;
const across = run(width - 2 * corner.w, repeat.h);      // top and bottom
const down = run(height - 2 * corner.h, repeat.v);       // left and right

for (let i = 0; i < across.count; i++) {
  const x = corner.w + i * across.step;
  const k = across.step / repeat.h;
  place("edge-h", `translate(${x} 0) scale(${k} 1)`);               // top
  place("edge-h", `translate(${x} ${height}) scale(${k} -1)`);      // bottom, mirrored
}

Leave the border its margin: the frame is drawn around the text, not over it, and slots[0] with role text-area gives the inset — for Warsh, x 5.4455, y 5.3355, w 57.4257, h 89.3289 inside 0 0 68.317 100.

mushaf-hafs-madinah-kabir does not tile

It has no slices block, because its border does not repeat cleanly and the project measured that rather than guessed. Check for slices before you reach for a frame, and fall back to scaling its color.svg.

Try it live

Switch a page between the eight muṣḥafs and the font markers, recolour any ink group, and watch a border tile itself from three files.

How this data is made

A scan asset is cropped from a page of a named scan, cleaned of text and paper, traced from one quadrant where it is symmetric, and vectorised into the grouped SVG above; every file records the scan, page, crop box and the SHA-256 of the source PDF. A font asset is the U+06DD glyph of a named open font, in a named weight, decomposed into the same group contract. The files on this page are the copies served under /demo/, built from quran-ws/quran-assets at 591e8f5; the full contract is in the Quran Assets reference.