Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Use an in-memory reader when the Go API expects io.Reader: wrap your CSS with strings.NewReader(cssText) or bytes.NewBufferString(cssText), then pass it to the parser. If the library accepts strings directly, pass the string without an adapter. Loading text for parsing is not the same as applying CSS in a browser or fetching linked stylesheets.

Choose the input shape your CSS library expects

Go strings are already in memory, so a temporary file is unnecessary for a reader-based parser. The standard-library adapters cover the two common cases:

  • strings.NewReader(cssText) returns an io.Reader over a string.
  • bytes.NewBufferString(cssText) returns a byte buffer that also satisfies io.Reader.

The tdewolff/parse/v2/css package documents a reader-oriented parser. The aymerick/douceur parser documents a direct string interface. Pick according to the output you need: token or grammar iteration versus a parsed stylesheet representation.

Parse a stylesheet string with tdewolff/parse

This example follows the v2 parser shape documented by the package. It treats the input as a complete stylesheet, not the contents of a style attribute.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
package main

import (
    "fmt"
    "io"
    "strings"

    "github.com/tdewolff/parse/v2"
    "github.com/tdewolff/parse/v2/css"
)

func main() {
    cssText := `body { color: rebeccapurple; }`

    input := parse.NewInput(strings.NewReader(cssText))
    p := css.NewParser(input, false) // false means a stylesheet, not inline declarations

    for {
        grammar, _, data := p.Next()
        if grammar == css.ErrorGrammar {
            break
        }

        // Inspect grammar/data, or call p.Values() when token values are needed.
        fmt.Printf("grammar=%v data=%qn", grammar, data)
    }

    if err := p.Err(); err != nil && err != io.EOF {
        panic(err)
    }
}

The important operation is the conversion from the string to a reader. The parser’s Next() method advances through grammar units. Stop on css.ErrorGrammar, then inspect Err(); do not assume that every stop means successful parsing. Check the exact version you select because APIs can change between dependency versions.

Use bytes.NewBufferString instead

input := parse.NewInput(bytes.NewBufferString(cssText))
p := css.NewParser(input, false)

Both adapters are in-memory. Choose strings.NewReader when you are starting with text and bytes.NewBufferString when surrounding code already works with byte buffers.

Set the inline flag correctly

The second argument to the documented constructor distinguishes context:

  • false: a complete stylesheet containing rules such as body { ... }.
  • true: declarations intended for an inline style attribute, such as color: red;.

Passing the wrong mode can produce unexpected grammar interpretation, especially when parsing declaration text rather than selectors and rules.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Parse a CSS string directly with Douceur

If you want a stylesheet object instead of manually iterating parser grammar, Douceur’s documented API accepts the string itself:

package main

import (
    "fmt"
    "log"

    "github.com/aymerick/douceur/parser"
)

func main() {
    cssText := `body { color: rebeccapurple; }`

    stylesheet, err := parser.Parse(cssText)
    if err != nil {
        log.Fatal(err)
    }

    fmt.Println(stylesheet.String())
}

Here there is no reader adapter because parser.Parse is designed to receive a string. Handle its returned error before using the stylesheet.

Parsing is different from loading CSS into a browser

In this context, “load” can mean several different operations:

Operation What it does What it does not establish
Parse a string Lexes text into tokens, grammar units, or a stylesheet object. It does not render a DOM or apply computed styles.
Inline CSS in HTML Rewrites CSS defined in an HTML document into inline style attributes. Douceur’s documented inliner does not fetch external stylesheets.
Browser rendering Evaluates HTML, CSS, scripts, resources and layout in a browser engine. A parser alone is not a browser.

If your HTML contains <link rel="stylesheet" href="theme.css">, reading a CSS string into a parser will not retrieve that URL. Fetch the resource yourself, apply your own security policy, and then pass the fetched bytes or text to the parser. For email generation, use an inliner only when the CSS is available in the HTML document and the library’s supported syntax meets your needs.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Reusable helper functions

Reader-based helper

func parseCSS(cssText string) error {
    input := parse.NewInput(strings.NewReader(cssText))
    p := css.NewParser(input, false)

    for {
        grammar, _, data := p.Next()
        if grammar == css.ErrorGrammar {
            break
        }
        _ = data // transform or collect data here
    }

    if err := p.Err(); err != nil && err != io.EOF {
        return err
    }
    return nil
}

Return errors to the caller instead of panicking in a server, worker, or command-line tool that must continue handling other inputs.

Direct-string helper

func stylesheetFromString(cssText string) (string, error) {
    stylesheet, err := parser.Parse(cssText)
    if err != nil {
        return "", err
    }
    return stylesheet.String(), nil
}

Keep the original input available if you need to report a line, selector, or source fragment in an application error message.

Common mistakes and fixes

Passing a string where an io.Reader is required

A Go string does not implement Read. Wrap it with strings.NewReader or bytes.NewBufferString before constructing the parser.

Using the inline mode for a full stylesheet

Set the tdewolff parser’s isInline argument to false for rules and at-rules. Reserve true for declaration text from an inline style attribute.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Ignoring parser termination and errors

The documented tdewolff loop stops at ErrorGrammar. Afterward, call Err() and distinguish normal end-of-input such as io.EOF from malformed input or another failure.

Expecting external imports or linked files to be fetched

Neither a reader adapter nor a parser automatically turns a URL into CSS. Resolve external resources explicitly, and do not assume Douceur’s inliner fetches them; its documentation says it parses CSS defined in the HTML document only.

Confusing parsing with sanitizing

A parser can recognize CSS syntax, but your application still needs a policy for untrusted input, resource URLs, custom properties, scripts in surrounding HTML, and any transformations you perform.

Assuming every CSS feature is supported

Verify the selected dependency’s version, supported CSS syntax, maintenance status and compatibility with your Go version. The documented examples establish the input APIs, not a complete compatibility matrix or performance benchmark.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Performance, memory and reliability considerations

  • String adapters avoid temporary-file I/O, but the CSS text remains resident in memory while the parser consumes it.
  • For one-shot parsing, create a fresh reader for each input. A reader’s position advances and cannot be reused as if it were new.
  • For large or streamed stylesheets, use an io.Reader that reads from the stream rather than first constructing one giant string, if the library supports that workflow.
  • Bound input size and execution time when CSS comes from users or remote services. Parsing errors should be observable and returned to the caller.
  • Do not claim a speed advantage for either adapter without measuring your own workload; the available documentation does not provide comparable benchmarks.

Or skip the browser setup

If your real goal is to obtain a rendered screenshot of a page whose CSS is already loaded, you do not need to build a browser-capture pipeline. ScreenshotNeo is a website screenshot API and MCP server. It accepts cookie and consent banners before capture and removes more than 60 known consent platforms, newsletter popups and chat widgets; each step can be disabled. Bot checks, CAPTCHAs, blank pages, timeouts, failed loads and cache hits are not billed, and response headers identify the page verdict and billing result.

A single request returns PNG, JPEG, WebP or PDF. The API supports full-page captures with lazy images loaded, CSS-selector element capture, dark mode, device presets or custom viewports, retina scale, custom CSS and JavaScript, click-before-capture actions, selector hiding, selector/delay/network-idle waits, request and resource blocking, headers, cookies, user agents, authorization, timezone, geolocation, transparent backgrounds, resizing, configurable-TTL caching, signed image links, asynchronous webhooks, bulk capture of up to 100 URLs per call, usage reporting and an OpenAPI specification.

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp

For request parameters and output details, see the ScreenshotNeo documentation.

import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"}, timeout=90)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);

An MCP server provides take_screenshot, get_page_info and capture_pdf tools for Claude, Cursor and other MCP clients. The Free plan includes 1,000 screenshots per month without a card; paid plans start at $5 for 3,000 shots, and every feature is available on every plan. Sign up free for ScreenshotNeo.

Free tools Windows power users keep installed

One-click scans. No signup required.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Quick decision guide

  • Need parser tokens or grammar callbacks? Use tdewolff/parse with strings.NewReader or bytes.NewBufferString.
  • Need a parsed stylesheet from text with a compact call? Use Douceur’s parser.Parse(cssText).
  • Need to inline CSS already present in HTML? Use the inliner workflow, while remembering it does not fetch external stylesheets.
  • Need a rendered page image or PDF rather than a syntax tree? Use a browser-capable service such as ScreenshotNeo.

Frequently Asked Questions

Do I need to write the CSS string to a file first?

No. A reader adapter exposes the in-memory string directly to a reader-based parser.

Which adapter should I use, strings.NewReader or bytes.NewBufferString?

Both provide in-memory reader input. Use the one that matches the surrounding types in your code.

Can a CSS parser apply styles to HTML?

No. Parsing creates tokens or a stylesheet representation; browser rendering or an HTML inliner is a separate operation.

Will Douceur fetch a stylesheet referenced by a link tag?

Its documented inliner handles CSS defined in the HTML document and does not fetch external stylesheets.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.