If you've ever taken a screenshot of a real-world webpage with Puppeteer, you've seen the ugly truth: cookie banners, sticky headers frozen at weird positions, collapsed FAQ accordions, and "Subscribe!" popups.
Here are four small Puppeteer helpers I use to get clean, presentable screenshots. Each is one function, drop-in ready.
1. Kill cookie banners
async function hideCookieBanners(page, customSelectors = []) {
await page.evaluate((selectors) => {
// Known cookie-banner elements
['CookieReportsPanel', 'CookieReportsOverlay', 'CookieReportsBannerAZ']
.forEach(id => document.getElementById(id)?.remove());
document.querySelectorAll('[class*="wscr"],[id*="CookieReport"]')
.forEach(el => el.remove());
// Reset body scroll locks the banner often sets
document.body.style.overflow = '';
document.documentElement.style.overflow = '';
// User-defined selectors (if they know their site)
selectors.forEach(sel => {
try {
document.querySelectorAll(sel).forEach(el => {
el.style.setProperty('display', 'none', 'important');
});
} catch {}
});
}, customSelectors);
}
2. Hide arbitrary elements
async function hideElements(page, selectors) {
if (!selectors.length) return;
await page.evaluate(sels => {
sels.forEach(sel => {
try {
document.querySelectorAll(sel).forEach(el => {
el.style.setProperty('display', 'none', 'important');
});
} catch {}
});
}, selectors);
}
Example: hide chat widgets, "cookies" bars, floating "Get started" buttons:
await hideElements(page, [
'#intercom-container',
'.crisp-client',
'[class*="newsletter-popup"]'
]);
3. Unfix sticky headers
Sticky headers repeat on every "screen" of a fullPage screenshot — ugly. Convert them to position: relative:
async function unfixSticky(page, selectors) {
await page.evaluate(sels => {
sels.forEach(sel => {
try {
document.querySelectorAll(sel).forEach(el => {
el.style.setProperty('position', 'relative', 'important');
['top', 'bottom', 'left', 'right', 'z-index']
.forEach(p => el.style.setProperty(p, 'auto', 'important'));
el.style.setProperty('width', '100%', 'important');
});
} catch {}
});
}, selectors);
}
4. Expand FAQs / accordions
Most FAQs use classes like _active or attributes like open. Give the function a CSS selector + action + value:
async function expandAccordions(page, pairs) {
if (!pairs.length) return;
await page.evaluate(rules => {
rules.forEach(({ selector, action, value }) => {
try {
document.querySelectorAll(selector).forEach(el => {
if (action === 'class') {
el.classList.add(value || '_active');
// Also unhide next sibling (common FAQ pattern)
const next = el.nextElementSibling;
if (next) {
next.style.display = 'block';
next.style.maxHeight = 'none';
next.removeAttribute('hidden');
}
} else if (action === 'attribute') {
const [name, val = ''] = value.split('=');
el.setAttribute(name, val);
} else if (action === 'style') {
value.split(';').forEach(rule => {
const [p, v] = rule.split(':');
if (p && v) {
el.style.setProperty(p.trim(), v.trim(), 'important');
}
});
}
});
} catch {}
});
}, pairs);
// Wait for CSS transitions
await new Promise(r => setTimeout(r, 400));
}
Example usage:
await expandAccordions(page, [
{ selector: '.faq__question', action: 'class', value: '_active' },
{ selector: 'details', action: 'attribute', value: 'open=true' },
{ selector: '.accordion-body', action: 'style', value: 'display: block' }
]);
Putting it together
await page.goto(url, { waitUntil: 'domcontentloaded' });
await hideCookieBanners(page);
await hideElements(page, ['.chat-widget']);
await unfixSticky(page, ['.sticky-nav']);
await expandAccordions(page, [{ selector: '.faq__q', action: 'class', value: '_open' }]);
await page.screenshot({ path: 'clean.png', fullPage: true });
Where I use this
All four helpers ship in production on Site2PDF — a website-to-PDF tool I'm building. Users paste a URL, tick what to hide/expand, and get a clean archive.
If you're doing any kind of web scraping or automated screenshots, grab these — they'll save you a weekend of CSS archaeology.
Top comments (0)