How to Preserve PDF Scroll Position When Navigating Tabs in Firefox and Chrome
When building single-page applications or tabbed interfaces that embed PDF documents using <object>, <embed>, or <iframe> tags, developers frequently run into an annoying UX issue: switching tabs resets the PDF viewer to page 1 and clears the scroll position. This issue is particularly persistent in Firefox due to how its built-in PDF viewer (PDF.js) responds to layout changes.
Why Does Firefox Reset Embedded PDF Scroll Positions?
The standard way to show and hide tabs in modern web design is by applying display: none to inactive tab containers. However, applying display: none removes the element entirely from the browser's layout tree. When the element is restored (by removing display: none), Firefox's native PDF renderer treats the element as though it has been newly rendered within the layout frame. Even if a load event isn't fired, the internal PDF viewer viewport resets its scroll offset to top-left (0, 0) and jumps back to the first page.
Chromium-based browsers like Chrome and Brave handle layout reflows for native plugins slightly differently, which is why your code might work in Chrome but fail in Firefox.
Solution 1: Use CSS Grid Stacking Instead of display: none (Recommended)
The simplest and most performant fix is to avoid removing the PDF container from the rendering tree. Instead of using display: none, you can overlay all tab panels in the same grid cell using CSS Grid and control visibility using visibility: hidden or opacity.
Because visibility: hidden keeps the element in the rendering pipeline without displaying it visually, Firefox maintains the PDF viewer state and exact scroll position.
Updated CSS
/* Container for all tab panels */
.body {
display: grid;
grid-template-columns: 1fr;
grid-template-rows: 1fr;
}
/* Force all tabs to occupy the exact same space */
.tab {
grid-area: 1 / 1;
visibility: hidden;
opacity: 0;
transition: opacity 0.15s ease-in-out;
}
/* Show only the active tab */
.tab.sel {
visibility: visible;
opacity: 1;
}Updated JavaScript
Since the elements now rely on visibility, your tab navigation code logic remains lightweight:
function Navigate(evt) {
let e = evt.target;
if (!e.matches('button, button *') || !(e = e.closest('button')) || e === activeTabBtn) return;
const currentTabIndex = parseInt(activeTabBtn.value);
const newTabIndex = parseInt(e.value);
body.children[currentTabIndex].classList.remove('sel');
body.children[newTabIndex].classList.add('sel');
activeTabBtn.classList.remove('sel');
e.classList.add('sel');
activeTabBtn = e;
}Solution 2: Open at a Specific Page Using URL Fragment Identifiers
If you must re-render or dynamically recreate the PDF element when switching tabs, you can instruct the browser PDF viewer to open directly at a specific page using standard Open Parameters for PDF specification.
By appending #page=PAGE_NUMBER or #zoom=100,0,200 to the PDF URL, most built-in browser viewers will scroll to that specific location on initialization:
// Example of updating the object data URL to open at page 3
const pdfUrl = "https://example.com/document.pdf#page=3";
object.setAttribute("data", pdfUrl);Note: While this helps navigate to a target page, it won't preserve fine-grained pixel-level horizontal or vertical scroll offsets unless using a full custom rendering engine.
Solution 3: Implement Custom PDF Rendering with PDF.js
If your web application requires full programmatic control over zoom levels, scroll offsets, page navigation events, and state persistence across all browsers, relying on native browser <object> tags has limitations due to cross-origin security boundaries around native browser viewers.
In these cases, embedding PDF.js directly into your application gives you full JS access to scroll position APIs via standard HTML5 <canvas> rendering.
Summary
- Avoid
display: none: It disrupts Firefox's native PDF viewport state. - Use CSS Grid &
visibility: hidden: Keeps embedded viewers active in the DOM without flickering or losing scroll state. - Use PDF URL Parameters: Append
#page=Nif you are dynamically mounting native PDF objects.