The Art of Full-Page Screenshots

Why your screenshots have "double headers" and how to fix them using CSS injection and smart scrolling.

đź“‹ Table of Contents

The "Double Header" Problem

You've just written a Puppeteer script to take a fullPage: true screenshot. You open the file, and... what's this? The navigation bar appears repeatedly throughout the image, like a glitch in a video game.

This happens because modern websites often use position: fixed or position: sticky for headers. When the browser synthesizes a full-page screenshot by scrolling and stitching images together, these fixed elements get captured in every frame.

Solution 1: CSS Injection

The cleanest way to fix this is to inject a small snippet of CSS before taking the screenshot. We want to change fixed elements to absolute, or hide them entirely if they aren't needed for the context.

/* Hide sticky headers */
header, .sticky, .fixed-top {
    position: absolute !important;
    top: 0 !important;
}

/* Or hide aggressive popups */
.cookie-banner, .modal {
    display: none !important;
}

In Playwright, you can inject this easily:

await page.addStyleTag({
    content: 'header { position: absolute !important; }'
});

Solution 2: JavaScript Cleanup

Sometimes you don't know the class names ahead of time. In that case, you can use a brute-force JavaScript approach to find all fixed headers and un-fix them.

await page.evaluate(() => {
    const elements = document.querySelectorAll('*');
    for (const el of elements) {
        const style = window.getComputedStyle(el);
        if (style.position === 'fixed' || style.position === 'sticky') {
            el.style.position = 'absolute';
        }
    }
});

Handling Lazy Loading

Another common issue is empty white spaces at the bottom of the page. This is usually caused by images that only load when they come into the viewport (Lazy Loading).

To fix this, you must simulate a human user scrolling down the page before you take the screenshot.

async function autoScroll(page){
    await page.evaluate(async () => {
        await new Promise((resolve) => {
            let totalHeight = 0;
            const distance = 100;
            const timer = setInterval(() => {
                const scrollHeight = document.body.scrollHeight;
                window.scrollBy(0, distance);
                totalHeight += distance;

                if(totalHeight >= scrollHeight - window.innerHeight){
                    clearInterval(timer);
                    resolve();
                }
            }, 100);
        });
    });
}

The Easy Way

Handling all these edge cases—sticky headers, cookie banners, lazy loading images, infinite scrolls—is a maintenance nightmare.

SnapAPI handles all of this automatically. Our engine detects fixed elements and handles the stitching logic so you get a clean, perfect image every time.

Stop Fighting with CSS

Get perfect full-page screenshots with a single API call.

Try SnapAPI Free →