JavaScript in EPUB 3: Scripted EPUBs, Interactivity, and Reader Support
EPUB 3 supports JavaScript — but with significant limitations compared to the web. Interactive EPUBs can include quizzes, collapsible content, and dynamic elements, but reader support is inconsistent and accessibility requires extra care. Here's what you need to know.
Enabling Scripting in EPUB 3
To use JavaScript in an EPUB, declare the content file as scripted in the OPF manifest:
<!-- In content.opf manifest -->
<item id="ch1" href="chapter01.xhtml"
media-type="application/xhtml+xml"
properties="scripted"/>
The scripted property tells reading systems this file contains scripts and may require JavaScript to render correctly. Reading systems that disable scripting will display a fallback.
Including JavaScript in XHTML
<!-- Inline script in XHTML -->
<script type="text/javascript">
//"<![CDATA[
document.getElementById('toggle-btn').addEventListener('click', function() {
var panel = document.getElementById('collapsible');
panel.style.display = panel.style.display === 'none' ? 'block' : 'none';
});
//]]>
</script>
<!-- External script -->
<script type="text/javascript" src="../scripts/interactive.js"></script>
The CDATA wrapping is required in XHTML to prevent XML parsers from treating JS operators (<, &) as XML markup.
Practical Interactive Patterns
Collapsible Section
<button id="toggle-btn" type="button"
aria-expanded="false"
aria-controls="collapsible">
Show answer
</button>
<div id="collapsible" hidden>
<p>The answer is 42.</p>
</div>
<script>
//"<![CDATA[
document.getElementById('toggle-btn').addEventListener('click', function() {
var panel = document.getElementById('collapsible');
var btn = document.getElementById('toggle-btn');
var shown = !panel.hidden;
panel.hidden = shown;
btn.setAttribute('aria-expanded', String(!shown));
});
//]]>
</script>
Simple Quiz
<form id="quiz">
<fieldset>
<legend>What does EPUB stand for?</legend>
<label><input type="radio" name="q1" value="a"/> Electronic Publication</label>
<label><input type="radio" name="q1" value="b"/> Extended PDF</label>
</fieldset>
<button type="submit">Check</button>
<p id="result" aria-live="polite"></p>
</form>
<script>
//"<![CDATA[
document.getElementById('quiz').addEventListener('submit', function(e) {
e.preventDefault();
var answer = document.querySelector('input[name="q1"]:checked');
document.getElementById('result').textContent =
answer && answer.value === 'a' ? 'Correct!' : 'Try again.';
});
//]]>
</script>
Reading System Support Matrix
| Reading System | JS Support | Notes |
|---|---|---|
| Apple Books (iOS/macOS) | Yes | Best JS support; full DOM access |
| Thorium Reader | Yes | Chromium-based; good support |
| Kindle (app) | Partial | Limited; avoid complex DOM manipulation |
| Kindle (e-ink device) | No | Scripts silently ignored |
| Kobo (device) | No | Scripts silently ignored |
| Adobe Digital Editions | No | Scripts stripped |
| Calibre Viewer | Yes | Good for testing |
Fallback Requirements
Because many readers don't support JavaScript, scripted EPUBs must degrade gracefully:
- Use
hiddenattribute (notdisplay:none) so content is accessible when JS is unavailable - Don't put critical content inside JS-generated elements — put it in HTML with JS enhancing it
- Use
aria-liveregions for dynamic content so screen readers catch updates - Test with JavaScript disabled in Calibre viewer to verify the fallback experience
What JavaScript Cannot Do in EPUB
- Access external URLs (network requests blocked in sandboxed reading systems)
- Persist data across sessions (localStorage may not be available)
- Access the filesystem or native device APIs
- Communicate between spine items (each XHTML file is isolated)