Go chromedp
last modified October 30, 2026
In this article we show how to automate a browser in Go with chromedp, a library that drives Chrome or Chromium through the DevTools protocol.
chromedp starts the browser in headless mode, loads pages, queries elements
with CSS selectors, clicks and types, evaluates JavaScript and takes
screenshots. No external driver such as ChromeDriver is needed. The examples
use chromedp 0.21.0, Chrome 156 and Go 1.27.0; that version of chromedp has a
functional API in which actions return their values and
chromedp.Run returns a single result.
The examples load a small local site. Save the two files below in a
site directory and serve it with
python3 -m http.server 8080.
Reading the page title
chromedp.NewContext creates a browser context, and
chromedp.Do runs a sequence of actions. The Title
action returns the document title.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Falcon shop</title>
</head>
<body>
<h1>Falcon shop</h1>
<ul>
<li class="product" data-sku="F-1">
<a href="/falcon.html">Peregrine falcon</a>
<span class="price">120</span>
</li>
<li class="product" data-sku="F-2">
<a href="/fox.html">Arctic fox</a>
<span class="price">90</span>
</li>
</ul>
</body>
</html>
The home page of the demo site.
package main
import (
"context"
"fmt"
"log"
"github.com/chromedp/chromedp"
)
func main() {
ctx, cancel := chromedp.NewContext(context.Background())
defer cancel()
if err := chromedp.Do(ctx,
chromedp.Navigate("http://127.0.0.1:8080/"),
); err != nil {
log.Fatal(err)
}
title, err := chromedp.Run(ctx, chromedp.Title())
if err != nil {
log.Fatal(err)
}
fmt.Println("title:", title)
}
NewContext starts Chrome on first use, and the deferred
cancel closes it. The title action is run separately because it
returns a string, while Do runs actions that return no value.
$ python3 -m http.server 8080 # in site/ $ go run main.go title: Falcon shop
Reading the text of an element
The Text action returns the visible text of the first element that
matches a selector. chromedp.CSS selects elements with a CSS
selector, which is the usual choice for scraping.
package main
import (
"context"
"fmt"
"log"
"github.com/chromedp/chromedp"
)
func main() {
ctx, cancel := chromedp.NewContext(context.Background())
defer cancel()
if err := chromedp.Do(ctx,
chromedp.Navigate("http://127.0.0.1:8080/"),
); err != nil {
log.Fatal(err)
}
heading, err := chromedp.Run(ctx, chromedp.Text(chromedp.CSS("h1")))
if err != nil {
log.Fatal(err)
}
fmt.Println("heading:", heading)
}
The heading is returned as a plain string with surrounding whitespace removed.
Query actions retry until an element matches, so a selector that never appears
keeps waiting instead of returning an empty string; give the context a deadline
with context.WithTimeout when that is a possibility.
$ go run main.go heading: Falcon shop
Reading an attribute
AttributeValue returns an AttributeResult with the
value and an Exists flag. The flag distinguishes an empty attribute
from a missing one.
package main
import (
"context"
"fmt"
"log"
"github.com/chromedp/chromedp"
)
func main() {
ctx, cancel := chromedp.NewContext(context.Background())
defer cancel()
if err := chromedp.Do(ctx,
chromedp.Navigate("http://127.0.0.1:8080/"),
); err != nil {
log.Fatal(err)
}
res, err := chromedp.Run(ctx,
chromedp.AttributeValue(chromedp.CSS("li.product"), "data-sku"))
if err != nil {
log.Fatal(err)
}
fmt.Println("sku:", res.Value, "found:", res.Exists)
}
The first product has the SKU F-1, and the flag confirms that the
attribute is present. Attributes are read from the element matched by the
selector, not from its children.
$ go run main.go sku: F-1 found: true
Clicking a link
Clicks are actions too. Click dispatches a real mouse click, and
WaitVisible waits until the next page has rendered the element
before the text is read.
package main
import (
"context"
"fmt"
"log"
"github.com/chromedp/chromedp"
)
func main() {
ctx, cancel := chromedp.NewContext(context.Background())
defer cancel()
if err := chromedp.Do(ctx,
chromedp.Navigate("http://127.0.0.1:8080/"),
chromedp.Click(chromedp.CSS(`a[href="/falcon.html"]`)),
chromedp.WaitVisible(chromedp.CSS("h1")),
); err != nil {
log.Fatal(err)
}
text, err := chromedp.Run(ctx, chromedp.Text(chromedp.CSS("h1")))
if err != nil {
log.Fatal(err)
}
location, err := chromedp.Run(ctx, chromedp.Location())
if err != nil {
log.Fatal(err)
}
fmt.Println("heading:", text)
fmt.Println("url:", location)
}
The click follows the link to the detail page, the wait covers the navigation, and the last two actions read the new heading and the new URL. The browser is still the same process, so the context is reused.
$ go run main.go heading: Peregrine falcon url: http://127.0.0.1:8080/falcon.html
Filling in a form
SendKeys types into a focused element, and Click
submits the form. The page updates a paragraph through JavaScript, which the
example then reads.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Falcon form</title>
</head>
<body>
<h1>Falcon form</h1>
<form onsubmit="event.preventDefault(); document.querySelector('#out').textContent = document.querySelector('#name').value + ' is ready';">
<input id="name" type="text" placeholder="name">
<button id="go" type="submit">Go</button>
</form>
<p id="out"></p>
</body>
</html>
The form page: the submit handler writes the typed value
into the paragraph with the id out.
package main
import (
"context"
"fmt"
"log"
"github.com/chromedp/chromedp"
)
func main() {
ctx, cancel := chromedp.NewContext(context.Background())
defer cancel()
if err := chromedp.Do(ctx,
chromedp.Navigate("http://127.0.0.1:8080/form.html"),
chromedp.SendKeys(chromedp.CSS("#name"), "Falcon"),
chromedp.Click(chromedp.CSS("#go")),
); err != nil {
log.Fatal(err)
}
out, err := chromedp.Run(ctx, chromedp.Text(chromedp.CSS("#out")))
if err != nil {
log.Fatal(err)
}
fmt.Println("result:", out)
}
The script runs in the page, so the result is visible in the DOM. If the form
posted to the server instead, WaitVisible or a navigation wait
would be needed before reading the response.
$ go run main.go result: Falcon is ready
Evaluating JavaScript
Evaluate is generic: the type argument tells chromedp how to decode
the value produced by the expression. Numbers, strings, booleans, slices and
maps all work.
package main
import (
"context"
"fmt"
"log"
"github.com/chromedp/chromedp"
)
func main() {
ctx, cancel := chromedp.NewContext(context.Background())
defer cancel()
if err := chromedp.Do(ctx,
chromedp.Navigate("http://127.0.0.1:8080/"),
); err != nil {
log.Fatal(err)
}
count, err := chromedp.Run(ctx,
chromedp.Evaluate[int](`document.querySelectorAll("li.product").length`))
if err != nil {
log.Fatal(err)
}
title, err := chromedp.Run(ctx,
chromedp.Evaluate[string](`document.title`))
if err != nil {
log.Fatal(err)
}
fmt.Println("products:", count)
fmt.Println("evaluated title:", title)
}
The first expression counts the product elements and the second one reads the document title. The Go type must match the JavaScript value, otherwise the decode fails.
$ go run main.go products: 2 evaluated title: Falcon shop
Taking a screenshot
EmulateViewport sets the viewport size, and
CaptureScreenshot returns the image as bytes. The example decodes
the PNG header to check the dimensions before writing the file.
package main
import (
"bytes"
"context"
"fmt"
"image"
_ "image/png"
"log"
"os"
"github.com/chromedp/chromedp"
)
func main() {
ctx, cancel := chromedp.NewContext(context.Background())
defer cancel()
if err := chromedp.Do(ctx,
chromedp.EmulateViewport(800, 600),
chromedp.Navigate("http://127.0.0.1:8080/"),
); err != nil {
log.Fatal(err)
}
buf, err := chromedp.Run(ctx, chromedp.CaptureScreenshot())
if err != nil {
log.Fatal(err)
}
cfg, format, err := image.DecodeConfig(bytes.NewReader(buf))
if err != nil {
log.Fatal(err)
}
if err := os.WriteFile("screenshot.png", buf, 0o644); err != nil {
log.Fatal(err)
}
fmt.Println("format:", format)
fmt.Println("size:", cfg.Width, "x", cfg.Height)
}
The saved screenshot.png is a PNG image of exactly 800 by 600
pixels. FullScreenshot would capture the entire scrollable page
instead of the viewport.
$ go run main.go format: png size: 800 x 600
Source
In this article we have shown how to automate Chrome with chromedp: starting a browser context, reading the title, text and attributes, clicking links, filling in forms, evaluating JavaScript, and taking screenshots.
Author
List all Go tutorials.