How to Screenshot Single-Page Applications (React, Vue, Angular)
Capture reliable screenshots of SPAs built with React, Vue, and Angular. Solve blank page issues with proper wait strategies and selector timing.
Last updated: 2026-03-25
Try ScreenshotAPI free
5 free credits. No credit card required.
Screenshotting single-page applications is harder than capturing static HTML pages. SPAs built with React, Vue, Angular, or Svelte render content dynamically with JavaScript after the initial page load. If you screenshot too early, you get a blank page or a loading spinner. This guide explains the problem and shows reliable solutions.
Why SPA Screenshots Fail
A traditional server-rendered page sends complete HTML to the browser. An SPA sends a minimal shell:
html<!DOCTYPE html> <html> <body> <div id="root"></div> <script src="/app.bundle.js"></script> </body> </html>
The rendering timeline looks like this:
- Browser downloads HTML (the empty shell)
- Browser downloads JavaScript bundle (500 KB - 2 MB)
- JavaScript parses and executes
- Framework mounts and renders components
- Components fetch data from APIs
- UI updates with fetched data
A screenshot tool that captures at step 1 or 2 gets a blank page. Capturing at step 4 may show a loading spinner. You need to wait for step 6.
Wait Strategies
Strategy 1: Wait for network idle
Wait until all network requests have completed:
javascript// Puppeteer await page.goto('https://myapp.com', { waitUntil: 'networkidle0' }); // Playwright await page.goto('https://myapp.com', { waitUntil: 'networkidle' });
Pros: Simple, no app changes needed. Cons: Fails on apps with WebSocket connections, polling, or analytics pings that never go idle.
Strategy 2: Wait for a specific selector
Wait for a DOM element that only exists after rendering:
javascript// Puppeteer await page.goto('https://myapp.com'); await page.waitForSelector('#dashboard-content', { timeout: 10000 }); // Playwright await page.goto('https://myapp.com'); await page.waitForSelector('#dashboard-content', { timeout: 10000 });
Pros: Precise, works with WebSocket-heavy apps. Cons: Requires knowing a CSS selector for the final content.
Strategy 3: Wait for a data attribute
Add a data attribute in your app when rendering is complete:
jsx// In your React app function App() { const [loaded, setLoaded] = useState(false); const { data } = useQuery(['dashboard'], fetchDashboard); useEffect(() => { if (data) setLoaded(true); }, [data]); return <div data-loaded={loaded}>{/* content */}</div>; }
Then wait for it:
javascriptawait page.waitForSelector('[data-loaded="true"]');
Pros: Explicit signal from the app that content is ready. Cons: Requires modifying the application code.
Strategy 4: Combine strategies
The most reliable approach combines multiple signals:
javascriptawait page.goto('https://myapp.com', { waitUntil: 'domcontentloaded' }); await page.waitForSelector('#app-content', { timeout: 15000 }); await page.waitForTimeout(500); // Brief pause for animations await page.screenshot({ path: 'screenshot.png' });
The Easy Way: ScreenshotAPI
ScreenshotAPI supports all wait strategies as query parameters, no Puppeteer or Playwright setup required.
Wait for network idle
bashcurl -G "https://screenshotapi.to/api/v1/screenshot" \ -d "url=https://myreactapp.com" \ -d "width=1440" \ -d "height=900" \ -d "waitUntil=networkidle" \ -d "type=png" \ -H "x-api-key: sk_live_your_api_key" \ --output screenshot.png
Wait for a selector
bashcurl -G "https://screenshotapi.to/api/v1/screenshot" \ -d "url=https://myreactapp.com" \ -d "width=1440" \ -d "height=900" \ -d "waitForSelector=#dashboard-content" \ -d "type=png" \ -H "x-api-key: sk_live_your_api_key" \ --output screenshot.png
Wait for selector with delay
bashcurl -G "https://screenshotapi.to/api/v1/screenshot" \ -d "url=https://myreactapp.com" \ -d "width=1440" \ -d "height=900" \ -d "waitForSelector=#dashboard-content" \ -d "delay=500" \ -d "type=png" \ -H "x-api-key: sk_live_your_api_key" \ --output screenshot.png
JavaScript example
javascriptasync function screenshotSPA(url, selector) { const params = new URLSearchParams({ url, width: '1440', height: '900', type: 'png', waitUntil: 'networkidle', ...(selector ? { waitForSelector: selector } : {}) }); const response = await fetch( `https://screenshotapi.to/api/v1/screenshot?${params}`, { headers: { 'x-api-key': process.env.SCREENSHOT_API_KEY } } ); return Buffer.from(await response.arrayBuffer()); } // React app with known content selector const image = await screenshotSPA('https://myapp.com/dashboard', '#dashboard-loaded');
Framework-Specific Tips
React
React apps typically mount into #root. Wait for child content:
bashcurl -G "https://screenshotapi.to/api/v1/screenshot" \ -d "url=https://react-app.com" \ -d "waitForSelector=#root > div" \ -d "type=png" \ -H "x-api-key: sk_live_your_api_key" \ --output react.png
Vue
Vue 3 apps mount into #app. Wait for the mounted content:
bashcurl -G "https://screenshotapi.to/api/v1/screenshot" \ -d "url=https://vue-app.com" \ -d "waitForSelector=#app > div" \ -d "type=png" \ -H "x-api-key: sk_live_your_api_key" \ --output vue.png
Angular
Angular apps typically use <app-root>. Wait for child content:
bashcurl -G "https://screenshotapi.to/api/v1/screenshot" \ -d "url=https://angular-app.com" \ -d "waitForSelector=app-root > *" \ -d "type=png" \ -H "x-api-key: sk_live_your_api_key" \ --output angular.png
Next.js (Client-Side Rendering)
Next.js pages that use use client render on the client. Wait for hydration:
bashcurl -G "https://screenshotapi.to/api/v1/screenshot" \ -d "url=https://nextjs-app.com/dashboard" \ -d "waitUntil=networkidle" \ -d "waitForSelector=[data-loaded]" \ -d "type=png" \ -H "x-api-key: sk_live_your_api_key" \ --output nextjs.png
Full-Page SPA Screenshots
Combine SPA wait strategies with full-page capture:
javascriptconst params = new URLSearchParams({ url: 'https://myapp.com', width: '1440', fullPage: 'true', waitUntil: 'networkidle', waitForSelector: '#content-loaded', type: 'png' }); const response = await fetch( `https://screenshotapi.to/api/v1/screenshot?${params}`, { headers: { 'x-api-key': process.env.SCREENSHOT_API_KEY } } );
Debugging Blank Screenshots
If you are still getting blank screenshots:
- Check the selector: Open the site in Chrome DevTools and verify your CSS selector matches an element that appears after rendering
- Increase the delay: Add
delay=3000to give slow APIs time to respond - Try networkidle: If
loadis not enough, switch tonetworkidle - Check for auth: If the page requires login, the screenshot will show a login page, not the authenticated content
Next Steps
- Read about full-page screenshots for capturing entire SPA pages
- See the JavaScript guide for more Node.js examples
- Set up visual regression testing for your SPA
- Check pricing for credit-based plans
Frequently asked questions
Why do my SPA screenshots show a blank page?
SPAs render content with JavaScript after the initial HTML loads. If the screenshot is taken before JavaScript executes and the UI renders, you capture an empty shell. Use waitForSelector or waitUntil=networkidle to wait for content.
What is the best wait strategy for React app screenshots?
Use waitForSelector with a CSS selector that targets your main content area (e.g., #root > div or [data-loaded=true]). This is more reliable than time-based delays because it waits for actual content rather than guessing how long rendering takes.
Can I screenshot a Vue or Angular app the same way?
Yes. All SPA frameworks produce the same rendering pattern: an empty HTML shell that JavaScript fills with content. The same wait strategies (waitForSelector, networkidle) work for React, Vue, Angular, Svelte, and any other SPA framework.
How do I handle lazy-loaded content in SPA screenshots?
For full-page screenshots of SPAs with lazy loading, combine fullPage=true with waitUntil=networkidle. The API scrolls the page to trigger lazy-loaded content and waits for all network requests to complete.
Related resources
Start capturing screenshots today
Create a free account and get 5 credits to try the API. No credit card required. Pay only for what you use.