How to take a website screenshot in Go with chromedp
Capture a browser viewport, one element, or a full page in Go with chromedp, then save the image. Includes runnable code and fixes for common issues.
Use chromedp to control Chrome from Go: create a context, navigate to the page, capture the viewport, an element, or the full page, then write the returned bytes to a file. Choose chromedp.CaptureScreenshot for the visible viewport, chromedp.Screenshot for the first element matching a CSS selector, and chromedp.FullScreenshot for a full-page image.
Install chromedp and prepare Chrome
Use a Go module and add chromedp:
go mod init example.com/screenshot
go get github.com/chromedp/chromedp
chromedp drives Chrome or Chromium through the Chrome DevTools Protocol. Install a compatible browser in the environment where the program runs. In a desktop session, chromedp can usually locate the browser. In a container or server, make sure the browser executable is installed and available; if needed, pass its path through chromedp’s allocator options.
For repeatable builds, pin the chromedp module version in your project’s go.mod and use a known browser version in your deployment image. The reviewed project sources are from the current main branch and do not establish a version compatibility matrix, so verify the versions you choose in your own environment.
Capture and save a full-page screenshot
This complete program navigates to a URL, captures the whole page, and saves the returned bytes as a PNG. Replace the target URL as needed.
package main
import (
"context"
"log"
"github.com/chromedp/chromedp"
)
func main() {
ctx, cancel := chromedp.NewContext(context.Background())
defer cancel()
var image []byte
err := chromedp.Run(ctx,
chromedp.Navigate("https://example.com"),
chromedp.FullScreenshot(&image, 100),
)
if err != nil {
log.Fatal(err)
}
if err := os.WriteFile("screenshot.png", image, 0o644); err != nil {
log.Fatal(err)
}
}
Add "os" to the imports in this example; the full import list is shown below so the program can be copied and run directly:
package main
import (
"context"
"log"
"os"
"github.com/chromedp/chromedp"
)
func main() {
ctx, cancel := chromedp.NewContext(context.Background())
defer cancel()
var image []byte
if err := chromedp.Run(ctx,
chromedp.Navigate("https://example.com"),
chromedp.FullScreenshot(&image, 100),
); err != nil {
log.Fatal(err)
}
if err := os.WriteFile("screenshot.png", image, 0o644); err != nil {
log.Fatal(err)
}
}
Run it with go run .. The file is written relative to the program’s current working directory. The program checks both the browser action and file write errors so navigation, capture, and filesystem failures are visible.
Choose viewport, element, or full-page capture
| What to capture | Action | Behavior |
|---|---|---|
| Visible browser viewport | chromedp.CaptureScreenshot(&image) |
Captures the current viewport; it does not request capture beyond it. |
| One page element | chromedp.Screenshot("#report", &image) |
Captures the first matching element’s bounds. A missing selector returns an error. |
| Entire page | chromedp.FullScreenshot(&image, 100) |
Captures beyond the viewport. The helper’s emulation behavior matters if device settings are in use. |
Capture the current viewport
Replace the capture action in the full program with chromedp.CaptureScreenshot(&image). The captured bounds depend on the current viewport and device scale settings.
Capture one element
Use a CSS selector for the target element, for example:
chromedp.Screenshot("main article", &image)
The action selects the first match. If the selector matches nothing, the action fails; confirm the selector against the rendered page and wait until the element exists before capturing.
Capture the full page
FullScreenshot takes a screenshot beyond the viewport. Its quality argument is in the range 0–100: quality 100 selects PNG, while other values select JPEG. Use 100 for a lossless PNG; choose a lower value when JPEG output and smaller image files are preferred.
The official example warns that FullScreenshot overrides device emulation settings. If a workflow uses emulated device or viewport settings, reset them with device.Reset when appropriate before continuing with later actions.
Wait for the page state you need
Navigation completing does not guarantee that every asynchronous component, lazy image, or client-rendered section is ready. Wait for a page-specific condition rather than relying on a fixed sleep as a general solution. For example, if the screenshot needs a report element, wait for that selector before capturing it:
err := chromedp.Run(ctx,
chromedp.Navigate("https://example.com/report"),
chromedp.WaitVisible("#report", chromedp.ByID),
chromedp.Screenshot("#report", &image),
)
Pick a condition that reflects the page’s actual readiness: a visible element, a known loading indicator disappearing, or application-specific state. A fixed delay can be useful for a known animation or timed transition, but it may be too short on a slow run and waste time on a fast one.
Browser options and protocol details
The high-level helpers cover common screenshots. The underlying Chrome DevTools Protocol screenshot command also supports format, quality, clipping, capture from surface, capture beyond the viewport, and speed optimization. The generated bindings document PNG as the default format and capture beyond the viewport as off unless requested. Use the lower-level protocol when you need a combination the helpers do not expose.
For custom browser startup, chromedp provides allocator options such as setting the browser executable path and launch flags. Keep browser configuration aligned with the deployment environment: headless server or container runs may require different flags and system dependencies than a local desktop. Avoid blindly adding flags from unrelated examples; validate them against the Chrome version and security requirements you deploy.
Write the bytes with an extension that matches the selected format. PNG output should use a .png extension, while JPEG output should use .jpg or .jpeg. Do not assume the helper’s quality number means the same image format for every helper: the PNG/JPEG behavior described above is specifically for FullScreenshot.
Or skip the browser setup
ScreenshotNeo offers a website screenshot API and MCP server. Its API accepts one GET request and returns an image or PDF; its Go example can call that endpoint with the standard library:
package main
import (
"context"
"io"
"log"
"net/http"
"net/url"
"os"
)
func main() {
endpoint := "https://api.screenshotneo.com/v1/shot"
params := url.Values{}
params.Set("access_key", "YOUR_API_KEY")
params.Set("url", "https://example.com")
req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, endpoint+"?"+params.Encode(), nil)
if err != nil {
log.Fatal(err)
}
res, err := http.DefaultClient.Do(req)
if err != nil {
log.Fatal(err)
}
defer res.Body.Close()
if res.StatusCode < 200 || res.StatusCode >= 300 {
log.Fatalf("screenshot request failed: %s", res.Status)
}
file, err := os.Create("shot.webp")
if err != nil {
log.Fatal(err)
}
defer file.Close()
if _, err := io.Copy(file, res.Body); err != nil {
log.Fatal(err)
}
}
See the ScreenshotNeo API documentation for request parameters and response details. Cookie banners, popups, and chat widgets are removed before capture; bot checks, blank pages, failed loads, and cache hits are not billed. Its MCP server lets AI agents take screenshots. The free plan includes 1,000 screenshots a month with no card, and paid plans start at $5 for 3,000. Learn about ScreenshotNeo, then sign up for 1,000 free screenshots a month with no card.
cURL, Python, and Node.js alternatives
These examples make the same one-call capture using ScreenshotNeo. Replace the placeholder API key and target URL. The API documentation is at screenshotneo.com/docs.
cURL
curl -G "https://api.screenshotneo.com/v1/shot" \
-d access_key=YOUR_API_KEY \
--data-urlencode url=https://example.com \
-o shot.webp
Python
import requests
r = requests.get(
"https://api.screenshotneo.com/v1/shot",
params={"access_key": "YOUR_API_KEY", "url": "https://example.com"},
timeout=90,
)
r.raise_for_status()
open("shot.webp", "wb").write(r.content)
Node.js
const q = new URLSearchParams({
access_key: 'YOUR_API_KEY',
url: 'https://example.com'
});
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`Screenshot request failed: ${res.status}`);
await import('node:fs/promises').then(fs => fs.writeFile('shot.webp', Buffer.from(await res.arrayBuffer())));
Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| Chrome executable cannot be found or launched | The browser is missing, its path is not discoverable, or the runtime lacks required dependencies. | Install Chrome or Chromium in the runtime, check its executable path, and configure the allocator with the correct path where necessary. |
| Navigation or capture returns an error | The target cannot be reached, Chrome failed, or the action timed out. | Log the returned error, check network access and the target URL, and use a context timeout appropriate for the page. |
| Element screenshot reports no matching node | The selector is wrong or the element has not rendered yet. | Check the selector in the page, then wait for the element or its application-specific ready state. |
| Screenshot is blank or content is missing | The page may still be loading client-side content, a delayed image, or a lazy section. | Wait for the content condition needed by the capture and make sure the chosen capture bounds include it. |
| Full-page capture changes viewport or device behavior | FullScreenshot overrides device emulation settings. |
Use device.Reset to reset emulation and viewport settings when the workflow needs them restored. |
| Output file cannot be created | The working directory is not writable or the output path is invalid. | Choose a writable path, create its parent directory, and check the error returned by os.WriteFile. |
| Image format does not match the extension | The selected capture helper or quality setting returned a different format than expected. | For FullScreenshot, quality 100 selects PNG and other quality values select JPEG; align the filename extension with the bytes. |
Performance, reliability, and cost
With chromedp, your service owns browser startup, memory and CPU use, concurrency, browser updates, and network access to target sites. Reusing a browser process can avoid repeated startup overhead, while separate contexts help isolate individual tasks. Bound concurrency to the capacity of the host, and use context deadlines so stuck pages do not hold resources indefinitely.
Capture only the bounds you need: a viewport or element image is often smaller and quicker to write than a full-page image. Full-page captures can be large on long documents. Wait for meaningful readiness conditions to reduce premature captures without imposing a delay on every page. Handle errors and write output atomically if downstream jobs must not observe partial files.
There is no fixed per-capture infrastructure cost for chromedp: expense depends on the compute, browser capacity, storage, and operational work you provide. ScreenshotNeo pricing is 1,000 shots a month free with no card; Starter is $5 for 3,000, Growth $15 for 15,000, Pro $39 for 60,000, Scale $99 for 250,000, and Business $249 for 1,000,000. Yearly billing gives two months free, and every feature is on every plan.
FAQ
Does chromedp capture the first matching element or every match?
chromedp.Screenshot captures the first element matching its selector.
Can I use chromedp for a full-page screenshot?
Yes. Use chromedp.FullScreenshot; it requests capture beyond the viewport.
Does successful navigation mean the page is fully rendered?
No. Wait for the page-specific content or state your screenshot requires.
Where can I find the ScreenshotNeo API parameters?
See the ScreenshotNeo documentation.


