GuidesThe EPUB format · Published 12 Jun 2026 · Updated 2 Sep 2026 · toolkit.bot

Inside an EPUB File: Structure, OPF, Navigation, Scripts and Pagination

June 12, 2026  ·  12 min read

An EPUB is not a mystery format — it's a ZIP archive containing a specific set of files. Once you understand the structure, you can fix broken EPUBs, build converters, or create files from scratch. This page walks through every part: the archive layout, the OPF package document, the two generations of navigation (NCX and nav document), what JavaScript can and cannot do, and how page breaks and print page numbers work in a format that has no fixed pages.

The EPUB ZIP Structure

mybook.epub (ZIP archive)
├── mimetype                    ← must be first, uncompressed
├── META-INF/
│   └── container.xml           ← points to the OPF file
└── OEBPS/  (or any folder name)
    ├── content.opf             ← package document (manifest + spine)
    ├── toc.ncx                 ← EPUB 2 navigation (NCX)
    ├── nav.xhtml               ← EPUB 3 navigation (NAV)
    ├── chapter01.xhtml         ← content files
    ├── chapter02.xhtml
    ├── css/
    │   └── styles.css
    └── images/
        ├── cover.jpg
        └── figure1.png

The mimetype File

The first file in the ZIP must be named mimetype, stored without compression, and contain exactly:

application/epub+zip

No newline, no BOM, no spaces. This is how e-readers and validators identify the file as an EPUB without reading the full archive. Creating EPUBs with Python:

import zipfile

with zipfile.ZipFile('book.epub', 'w') as z:
    # mimetype MUST be first and uncompressed
    z.writestr(zipfile.ZipInfo('mimetype'), 'application/epub+zip',
               compress_type=zipfile.ZIP_STORED)
    # All other files can be compressed
    z.write('META-INF/container.xml', compress_type=zipfile.ZIP_DEFLATED)
    z.write('OEBPS/content.opf', compress_type=zipfile.ZIP_DEFLATED)

META-INF/container.xml

This file tells the reading system where to find the OPF package document:

<?xml version="1.0" encoding="UTF-8"?>
<container version="1.0" xmlns="urn:oasis:names:tc:opendocument:xmlns:container">
  <rootfiles>
    <rootfile full-path="OEBPS/content.opf"
              media-type="application/oebps-package+xml"/>
  </rootfiles>
</container>

The full-path is relative to the root of the ZIP. The OEBPS folder name is conventional but not required — you can use any folder name or put the OPF in the root. An EPUB 3 file has exactly one OPF (the multiple-renditions experiment in EPUB 3.0.1 was dropped in EPUB 3.2).

The OPF Package Document (content.opf)

OPF stands for Open Packaging Format. The .opf file (historically content.opf or package.opf) is the heart of an EPUB and the first thing a reading system parses. It answers three questions — what is this book, what files does it contain, and in what order are they read — through four sections:

<?xml version="1.0" encoding="UTF-8"?>
<package xmlns="http://www.idpf.org/2007/opf"
         xmlns:dc="http://purl.org/dc/elements/1.1/"
         version="3.0" unique-identifier="bookid">
  <metadata>
    <dc:identifier id="bookid">urn:uuid:a1b2c3d4-e5f6-7890-abcd-ef1234567890</dc:identifier>
    <dc:title>My Book Title</dc:title>
    <dc:language>en</dc:language>
    <dc:creator>Author Name</dc:creator>
    <meta property="dcterms:modified">2026-06-12T00:00:00Z</meta>
  </metadata>
  <manifest>
    <item id="nav" href="nav.xhtml" media-type="application/xhtml+xml" properties="nav"/>
    <item id="ncx" href="toc.ncx" media-type="application/x-dtbncx+xml"/>
    <item id="ch1" href="chapter01.xhtml" media-type="application/xhtml+xml"/>
    <item id="ch2" href="chapter02.xhtml" media-type="application/xhtml+xml"/>
    <item id="css" href="css/styles.css"  media-type="text/css"/>
    <item id="cover-img" href="images/cover.jpg" media-type="image/jpeg"
          properties="cover-image"/>
  </manifest>
  <spine toc="ncx">
    <itemref idref="nav" linear="no"/>
    <itemref idref="ch1"/>
    <itemref idref="ch2"/>
  </spine>
</package>

Metadata

Required in every EPUB: dc:identifier (must match the unique-identifier attribute on <package>; a UUID or ISBN), dc:title, and dc:language (a BCP 47 code such as en, fr, zh-Hans). EPUB 3 also requires meta property="dcterms:modified" — an ISO 8601 UTC timestamp that must change on every revision. dc:creator, dc:publisher, dc:description, dc:subject and dc:date are optional. Accessibility metadata uses schema.org properties in the same block:

<meta property="schema:accessMode">textual</meta>
<meta property="schema:accessibilityFeature">structuralNavigation</meta>
<meta property="schema:accessibilityHazard">none</meta>
<meta property="schema:accessibilitySummary">This publication meets WCAG 2.2 Level AA.</meta>

Manifest

Every file in the archive except mimetype and the contents of META-INF/ must have an <item> with an id, an href relative to the OPF, and a media-type. A file in the ZIP that is missing from the manifest is an EPUBCheck error, and reading systems may ignore it. The optional properties attribute flags special items: nav, cover-image, scripted, mathml, svg. Common media types: application/xhtml+xml (content and nav document), text/css, image/jpeg, image/png, image/svg+xml, font/otf, font/woff2, application/x-dtbncx+xml (NCX).

Spine

The spine lists the XHTML documents in default reading order by idref. CSS, images, fonts and the nav document do not need spine entries. linear="no" marks items outside the main reading flow (cover page, table of contents) that readers may skip in sequential navigation. The toc="ncx" attribute points at the NCX for EPUB 2 readers.

What EPUB 3 changed

Content Files — XHTML, Not HTML

Chapter files must be valid XHTML — XML-conformant HTML. Key differences from HTML5:

<?xml version="1.0" encoding="UTF-8"?>
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:epub="http://www.idpf.org/2007/ops">
<head>
  <title>Chapter 1</title>
  <link rel="stylesheet" type="text/css" href="../css/styles.css"/>
</head>
<body>
  <section epub:type="chapter">
    <h1>Chapter 1: Introduction</h1>
    <p>Text here.</p>
  </section>
</body>
</html>

Navigation: NCX vs Nav Document

EPUB has two generations of table-of-contents file. The NCX (Navigation Control for XML, from the DAISY Consortium) was the EPUB 2 format — a custom XML file, usually toc.ncx, made of nested navPoint elements. EPUB 3 declared it obsolete and replaced it with the navigation document: an ordinary XHTML file whose <nav> elements are typed with epub:type.

<!-- toc.ncx (EPUB 2) -->
<navMap>
  <navPoint id="np1" playOrder="1">
    <navLabel><text>Chapter 1</text></navLabel>
    <content src="chapter01.xhtml"/>
  </navPoint>
</navMap>

<!-- nav.xhtml (EPUB 3) -->
<nav epub:type="toc" id="toc">
  <h1>Contents</h1>
  <ol>
    <li><a href="chapter01.xhtml">Chapter 1</a></li>
    <li><a href="chapter02.xhtml">Chapter 2</a></li>
  </ol>
</nav>
<nav epub:type="landmarks" hidden="">
  <ol>
    <li><a epub:type="toc" href="#toc">Table of Contents</a></li>
    <li><a epub:type="bodymatter" href="chapter01.xhtml">Begin Reading</a></li>
  </ol>
</nav>
FeatureNCX (EPUB 2)Nav document (EPUB 3)
File formatCustom XMLStandard XHTML, readable in a browser
Nested TOCNested navPointNested ol
Page listpageTargetnav epub:type="page-list"
LandmarksnavListnav epub:type="landmarks"
AccessibilityLimitedFull ARIA and WCAG support
StatusObsolete, still permittedRequired in EPUB 3

The nav document is required in EPUB 3 and read by every modern reader (Kindle, Kobo, Apple Books, Thorium, Calibre). The NCX is optional but harmless: include it for first-generation Kindles and some library systems. Declare it in the manifest with media-type="application/x-dtbncx+xml" and reference it with <spine toc="ncx">; EPUBCheck warns about it in strict EPUB 3 mode but does not fail. When both are present, EPUB 3 readers use the nav document and EPUB 2 readers fall back to the NCX. The optional landmarks nav gives screen readers jump points (cover, toc, bodymatter, backmatter); the page-list nav is covered under pagination below.

Scripting: JavaScript in EPUB 3

EPUB 3 allows JavaScript, but with much tighter limits than the web. A content file that contains scripts must be flagged in the manifest, and inline scripts must be wrapped in CDATA so the XML parser does not choke on < and & operators:

<!-- content.opf -->
<item id="ch1" href="chapter01.xhtml" media-type="application/xhtml+xml"
      properties="scripted"/>

<!-- chapter01.xhtml -->
<button id="toggle-btn" type="button" aria-expanded="false" aria-controls="answer">Show answer</button>
<div id="answer" hidden=""><p>The answer is 42.</p></div>
<script>
//<![CDATA[
document.getElementById('toggle-btn').addEventListener('click', function () {
  var panel = document.getElementById('answer');
  panel.hidden = !panel.hidden;
  this.setAttribute('aria-expanded', String(!panel.hidden));
});
//]]>
</script>
Reading systemJavaScript
Apple BooksYes — best support, full DOM access
Thorium Reader, Calibre viewerYes — Chromium-based, good for testing
Kindle appPartial — avoid complex DOM manipulation
Kindle and Kobo e-ink devicesNo — scripts silently ignored
Adobe Digital EditionsNo — scripts stripped

Because most e-ink devices ignore scripts, a scripted EPUB must degrade gracefully: keep all content in the HTML and let scripts enhance it, use the hidden attribute rather than display:none so content is reachable without JavaScript, announce dynamic changes with aria-live, and test with scripting disabled. Scripts cannot make network requests, rely on localStorage, touch the filesystem or device APIs, or communicate between spine items — each XHTML file runs in isolation.

Pagination: Page Breaks and Page Numbers

EPUB is reflowable — there are no fixed pages, and "page 42" depends on the screen and font size. Reading systems still paginate, and two mechanisms let authors control that: CSS break properties decide where a screen page may end, and static page markers record where the print pages began.

CSS page breaks

Use both the modern and legacy property names — Kindle historically honours page-break-before: always more reliably than break-before: page:

h1.chapter-title { page-break-before: always; break-before: page; }
table, figure    { page-break-inside: avoid;  break-inside: avoid; }
h2, h3           { page-break-after: avoid;   break-after: avoid; }   /* keep heading with next paragraph */
p                { orphans: 3; widows: 3; }                            /* min lines at page bottom / top */

Kobo, Apple Books, Thorium and Calibre support all of these; Kindle supports the legacy properties, only partially supports break-inside: avoid, and ignores orphans/widows. The most reliable chapter break is not CSS at all: put each chapter in its own XHTML file in the spine, because every reading system starts a new spine item on a fresh page. CSS breaks only apply to block elements — to break mid-paragraph, split it into two <p> elements.

Print page numbers: pagebreak markers and the page-list nav

To keep print page references usable in reflowable text — for academic citation, cross-referencing a print edition, or EPUB Accessibility 1.1 conformance for books that have a print equivalent — insert an invisible marker where each print page began, and list them in a page-list nav:

<!-- in the chapter, where print page 47 begins -->
<span epub:type="pagebreak" role="doc-pagebreak" id="page47" aria-label="Page 47"></span>

<!-- nav.xhtml -->
<nav epub:type="page-list" hidden="">
  <ol>
    <li><a href="chapter01.xhtml#page1">1</a></li>
    <li><a href="chapter02.xhtml#page47">47</a></li>
  </ol>
</nav>

Screen readers announce the aria-label, and reading systems that support the page list offer "go to page" navigation. The hidden="" attribute keeps the list out of the visual flow. Note that these markers are unrelated to the page counts consumer readers display — Kindle's page numbers are estimated from a standard font size, and most apps show a percentage or time remaining instead.

Inspecting an EPUB

# List contents without extracting
unzip -l book.epub

# Extract to a folder
unzip book.epub -d book_extracted/

# View OPF
unzip -p book.epub OEBPS/content.opf | xmllint --format -

EPUBs from PDF Conversion

When toolkit.bot converts a PDF to EPUB, it generates all required files: mimetype, container.xml, content.opf, a nav document plus a toc.ncx fallback, chapter XHTML files (one spine item per chapter, with page-break CSS on headings and break-inside: avoid on tables and figures), epub:type="pagebreak" markers and a page-list nav at the original PDF page boundaries, embedded images, and a stylesheet. The output passes EPUBCheck validation and includes EPUB Accessibility 1.1 metadata.

Convert your PDF to a valid EPUB3 →