Reading the Quran is not a single session. People read a page or a Juz each day. They leave. They come back tomorrow and want to continue from where they stopped. This is such a basic requirement that I'm surprised how many Quran apps get it wrong — or don't implement it at all.
Tilawa's last-read bookmark uses localStorage. When a user plays audio for an Ayah, or scrolls past it with audio playing, the position is recorded:
interface LastRead {
surahId: number;
surahName: string;
ayahNumberInSurah: number;
}
// Saved to localStorage under key "tilawa-last-read"
localStorage.setItem("tilawa-last-read", JSON.stringify({
surahId: currentSurahId,
surahName: currentSurahName,
ayahNumberInSurah: currentAyahNumberInSurah,
}));On the homepage, a useEffect reads this key. If a last-read position exists, a "Continue Reading" banner appears above the Surah list, showing the Surah name and Ayah number with a button to navigate directly to it.
The banner is unobtrusive — it sits at the top of the Surah list but doesn't push the list far down. On mobile it's a compact card. On desktop it's a thin highlighted bar.
The Jump to Ayah feature is a text input and button on each Surah page. The user types a number and clicks Jump (or presses Enter). The page calls scrollIntoView on the matching Ayah card element with { behavior: 'smooth', block: 'center' }, then briefly applies a highlight class to make it obvious which Ayah was jumped to.
The highlight is a CSS animation — a brief amber background flash that fades out over 1.5 seconds:
@keyframes ayah-highlight {
0% { background-color: rgba(245, 158, 11, 0.3); }
100% { background-color: transparent; }
}
.ayah-highlighted {
animation: ayah-highlight 1.5s ease-out;
}The URL hash feature was the most satisfying to build. Each Ayah card has an id attribute: id="ayah-255" for Ayah 255 of whatever Surah. The share button for each Ayah constructs a link like /surah/2#ayah-255. When someone follows this link, the browser's native anchor behavior scrolls to that element. But I also added a ScrollToAyah client component that reads window.location.hash on mount, finds the element, and calls scrollIntoView — because the native anchor scroll sometimes fires before the full list has rendered, and ends up scrolling to the wrong position.
The InitializeLastRead client component updates localStorage on audio play events. It's a thin client component that sits inside the Surah page but doesn't affect rendering — it just subscribes to the QuranContext and writes to localStorage when the current Ayah changes.
These features together — last read, jump to ayah, URL hash navigation, and the audio engine keeping track of position — create what I'd call the minimum viable continuous reading experience. You can hand your phone to someone, they can tap an Ayah in the middle of Al-Baqarah, and the audio will start from exactly that Ayah and carry forward automatically.
