The first version of Tilawa's audio was a single <audio> tag per Surah page. You'd click the play button next to an Ayah, it would play that one Ayah's audio file from alquran.cloud, and when it ended, nothing happened. You had to click the next Ayah manually.
That was fine for testing. It was not fine for actually using the app.
Continuous recitation — where the audio plays from Ayah 1 through to the last Ayah of the Surah without you touching anything — required a different architecture entirely.
The core problem is this: each Ayah has a separate audio file. Mishary Alafasy's recitation of Al-Baqarah is not one 2-hour file. It's 286 separate files, one per Ayah. To play Surah Al-Baqarah continuously, you need to:
- Start Ayah 1's audio
- When Ayah 1 ends, immediately start Ayah 2's audio
- Highlight the current Ayah so the reader knows where the reciter is
- Continue until the last Ayah
- If the user changes reciter mid-Surah, pause, swap the audio source, and resume at the same position
I built this as a React context (QuranContext.tsx) that wraps the entire application. A single useRef holds the HTMLAudioElement — not a React state, because changing an audio element's state (volume, src, currentTime) does not need to trigger a re-render. Only the metadata about what's playing (current Surah, current Ayah number, isPlaying boolean) is stored in React state, because those values drive the UI.
The autoplay chain works on the onended event:
audioRef.current.onended = () => {
if (loop) {
audioRef.current!.currentTime = 0;
audioRef.current!.play();
} else if (autoplayNext) {
playNextAyah();
}
};playNextAyah() increments the current Ayah index, constructs the new audio URL from the alquran.cloud CDN pattern, sets it as the src, and calls .play(). There is a useEffect that watches currentAyahNumber and calls scrollIntoView on the ayah card so the highlighted verse follows the audio.
The speed control was a one-liner — audioRef.current.playbackRate = rate — but exposing it in the UI cleanly took more thought. The speed menu is a floating popover that appears above the speed button on mobile and below on desktop.
The reciter switch was the trickiest part. If the user changes from Alafasy to Sudais mid-Surah while audio is playing, I need to:
- Note the current Ayah number
- Construct the new URL for the same Ayah with the new reciter ID
- Set the new src, reset currentTime to 0
- If it was playing before, autoplay it immediately
This broke three times before I got it right. The bug was a race condition: setting .src and calling .play() in the same synchronous tick sometimes resulted in the play() call happening before the browser had loaded the new src. The fix was wrapping the .play() in the oncanplay event handler.
One thing I'm still not fully happy with is the gap between Ayahs. There's a tiny silence — maybe 300–500ms — between one Ayah ending and the next beginning, caused by the network fetch latency for the next audio file. On a fast connection it's barely noticeable. On a 4G connection it's a small pause. The real solution would be preloading the next Ayah's audio while the current one is playing. That's on the roadmap.
