diff --git a/crates/ui-egui/src/canvas.rs b/crates/ui-egui/src/canvas.rs
--- a/crates/ui-egui/src/canvas.rs
+++ b/crates/ui-egui/src/canvas.rs
@@ -59,6 +59,27 @@ const MAX_CACHED_PAGES: usize = 24;
 /// `MAX_CACHED_PAGES` so raster tuning never silently retunes text memory.
 const MAX_CACHED_TEXT_PAGES: usize = 24;
 
+/// The page indicator's text: `4 / 42`, or `iv (4 / 42)` when the page has a label that is not
+/// its number (as Acrobat's page box does). Out of range (no pages) reads `0 / 0`.
+fn page_indicator_text(current: usize, count: usize, label: &str) -> String {
+    let number = if current < count { current + 1 } else { count };
+    if label.is_empty() || label == number.to_string() || current >= count {
+        format!("{number} / {count}")
+    } else {
+        format!("{label} ({number} / {count})")
+    }
+}
+
+/// A small pill at the bottom centre of the document area; display only, it takes no input.
+fn paint_page_indicator(ui: &egui::Ui, avail: Rect, text: &str) {
+    let t = Tokens::get(ui.ctx());
+    let painter = ui.painter().with_clip_rect(avail);
+    let galley = painter.layout_no_wrap(text.to_owned(), theme::medium(12.0), t.text);
+    let pill = Rect::from_center_size(pos2(avail.center().x, avail.bottom() - 22.0), galley.size() + vec2(20.0, 10.0));
+    painter.rect(pill, CornerRadius::same(255), t.chrome.gamma_multiply(0.92), Stroke::new(1.0, t.border), egui::StrokeKind::Inside);
+    painter.galley(pill.center() - galley.size() / 2.0, galley, t.text);
+}
+
 fn rgba_bytes(width: usize, height: usize) -> usize {
     width.saturating_mul(height).saturating_mul(4)
 }
@@ -4298,6 +4319,10 @@ pub fn document_area(app: &mut PdfCraftApp, index: usize, ui: &mut egui::Ui) {
 
     view.viewport_screen = out.inner_rect;
     view.auto_scroll.paint(ui, avail);
+    if app.show_page_indicator {
+        let label = info.pages.get(view.current).map_or("", |p| p.label.as_str());
+        paint_page_indicator(ui, avail, &page_indicator_text(view.current, info.pages.len(), label));
+    }
     if unobstructed && matches!(tool, QuickTool::Select | QuickTool::ColumnSelect) {
         copy_selected |= selection_actions(ui, avail, view, info, prefs, allowed && !comments_hidden, can_copy);
     }
@@ -7842,6 +7867,17 @@ trailer << /Root 1 0 R >>
         assert_eq!((v.current, v.goto, v.goto_point), (1, Some((1, 0.0)), None));
     }
 
+    #[test]
+    fn the_page_indicator_reads_page_over_pages_with_the_label_when_it_differs() {
+        assert_eq!(page_indicator_text(3, 42, ""), "4 / 42");
+        assert_eq!(page_indicator_text(3, 42, "4"), "4 / 42");
+        assert_eq!(page_indicator_text(3, 42, "iv"), "iv (4 / 42)");
+        // No pages, or a page past the end: never beyond the last.
+        assert_eq!(page_indicator_text(0, 0, ""), "0 / 0");
+        assert_eq!(page_indicator_text(9, 3, "x"), "3 / 3");
+        assert_eq!(page_indicator_text(usize::MAX, 3, ""), "3 / 3");
+    }
+
     #[test]
     fn destinations_set_the_zoom_mode_and_the_point_to_show() {
         let info = pdfcraft_render::DocInfo {
diff --git a/crates/ui-egui/src/js_ui.rs b/crates/ui-egui/src/js_ui.rs
--- a/crates/ui-egui/src/js_ui.rs
+++ b/crates/ui-egui/src/js_ui.rs
@@ -440,6 +440,7 @@ pub(crate) fn preferences_body(ui: &mut egui::Ui, app: &mut PdfCraftApp, t: &Tok
             .color(t.text_muted),
     );
     ui.add(widgets::checkbox(&mut app.always_default_view, tl!("Always use my default zoom and page layout")));
+    ui.add(widgets::checkbox(&mut app.show_page_indicator, tl!("Show the current page number over the document")));
     ui.horizontal(|ui| {
         ui.label(tl!("Units"));
         for u in crate::units::Unit::ALL {
diff --git a/crates/ui-egui/src/lib.rs b/crates/ui-egui/src/lib.rs
--- a/crates/ui-egui/src/lib.rs
+++ b/crates/ui-egui/src/lib.rs
@@ -740,6 +740,9 @@ pub struct PdfCraftApp {
     /// Preferences: open every document at Default zoom and Default page display, whatever
     /// layout or zoom it asks for (#1618).
     pub always_default_view: bool,
+    /// Preferences: a `page / pages` indicator floating over the document, whatever the
+    /// side panels show. Display only; nothing is written to the document.
+    pub show_page_indicator: bool,
     /// Open documents' files, watched for changes on disk.
     #[cfg(not(target_arch = "wasm32"))]
     pub(crate) watch: file_watch::FileWatch,
@@ -1160,6 +1163,7 @@ impl PdfCraftApp {
             reload_changed_files: true,
             show_secured_notice: true,
             always_default_view: false,
+            show_page_indicator: false,
             #[cfg(not(target_arch = "wasm32"))]
             watch: Default::default(),
             #[cfg(not(target_arch = "wasm32"))]
@@ -2642,6 +2646,7 @@ impl PdfCraftApp {
             "default_layout": canvas::display_token(self.view_defaults.layout, self.view_defaults.continuous),
             "default_zoom": self.view_defaults.zoom_name(),
             "always_default_view": self.always_default_view,
+            "show_page_indicator": self.show_page_indicator,
             "highlight_fields": self.view_defaults.highlight_fields,
             "units": self.units,
             "language": self.language,
@@ -2768,6 +2773,9 @@ impl PdfCraftApp {
         if let Some(on) = v["always_default_view"].as_bool() {
             self.always_default_view = on;
         }
+        if let Some(on) = v["show_page_indicator"].as_bool() {
+            self.show_page_indicator = on;
+        }
         if let Some(paper) = v
             .get("print_paper")
             .and_then(serde_json::Value::as_str)
@@ -3071,6 +3079,13 @@ impl PdfCraftApp {
                     _ => return Err("always-default-view must be on or off".into()),
                 };
             }
+            ("page-indicator", _) => {
+                self.show_page_indicator = match value {
+                    "on" => true,
+                    "off" => false,
+                    _ => return Err("page-indicator must be on or off".into()),
+                };
+            }
             ("tools-side", _) => {
                 self.side_panels.tools_side = match value {
                     "left" => side_panels::ToolsSide::Left,
