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

With Grover, pass your CSS string as the content of a style tag: style_tag_options: [{ content: css_string }]. Grover inserts that text into the HTML before Chromium renders the PDF, so you do not need to write a temporary stylesheet file.

Pass a CSS string to Grover

Grover’s documented inline-HTML API accepts style-tag options. The content value is the CSS text you want Chromium to apply.

require "grover"

css = <<~CSS
  @page {
    size: A4;
    margin: 18mm;
  }

  body {
    color: #222;
    font-family: Arial, sans-serif;
    font-size: 12pt;
  }

  h1 {
    color: #b3261e;
    page-break-after: avoid;
  }

  .notice {
    background: #fff3cd;
    border: 1px solid #e0b400;
    padding: 12px;
  }
CSS

html = <<~HTML
  <!doctype html>
  <html>
    <head>
      <meta charset="utf-8">
      <title>Invoice</title>
    </head>
    <body>
      <h1>Invoice 1007</h1>
      <p class="notice">Payment received.</p>
    </body>
  </html>
HTML

pdf = Grover.new(
  html,
  style_tag_options: [{ content: css }]
).to_pdf

File.binwrite("invoice.pdf", pdf)

This uses the same form shown in the Grover README: style_tag_options: [{ content: css_string }]. The result of to_pdf is binary PDF data, so write it with File.binwrite.

Why a CSS string is useful

Keeping CSS in a Ruby variable is practical when styles are generated from data, selected by a theme, or stored in a database or configuration service. It also avoids creating and cleaning up a temporary file for every job. The important distinction is between supplying CSS text and loading a stylesheet file: Grover’s content option is for text; url and path options are for separately stored stylesheets.

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

Make the HTML and CSS deterministic

Keep the style text separate

Build the HTML and CSS independently, then pass both to Grover. This makes it easier to test the generated markup and to swap themes without concatenating unescaped fragments into the document.

Escape data inserted into HTML

CSS injection and malformed markup are separate concerns from PDF rendering. Escape user-controlled values before putting them in HTML, and validate any value that is interpolated into CSS (for example, a color or a length). Do not treat a CSS string as a safe place to interpolate arbitrary input.

Use print-aware CSS

Chromium renders a print document, not an interactive browser window. Put page rules in @page, avoid relying on hover states, and use page-break properties where a heading or table row must stay together. Confirm the output with the same Chromium environment used in production.

Loading a stylesheet file instead of a string in Grover

If your CSS already lives in a file or at a URL, use Grover’s documented path or url stylesheet options instead of reading it into a string solely to pass it as content. Relative references are a common source of missing styles. For direct conversions, Grover documents using a display_url or absolute paths; without a usable base, Chromium resolves relative paths against its default display URL, http://example.com.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
pdf = Grover.new(
  html,
  display_url: "https://app.example.test",
  style_tag_options: [
    { path: "/absolute/path/to/print.css" }
  ]
).to_pdf

The exact option names and examples are maintained in the Grover README. Use a filesystem path that the rendering process can read, or a URL reachable from the machine running Chromium.

Using PDFKit when your CSS is already a string

PDFKit’s README documents creating a kit from HTML and adding stylesheet file paths with kit.stylesheets << '/path/to/css/file'. It does not document a dedicated CSS-string parameter. The straightforward HTML-level solution is to put the string inside a <style> element before passing the HTML to PDFKit.

require "pdfkit"

css = ".body { background: #f4f4f4; color: #222; }"
html = <<~HTML
  <!doctype html>
  <html>
    <head>
      <style>#{css}</style>
    </head>
    <body class="body">
      <h1>Report</h1>
    </body>
  </html>
HTML

kit = PDFKit.new(html)
File.binwrite("report.pdf", kit.to_pdf)

For linked CSS, PDFKit advises complete paths for images, stylesheets, and JavaScript in raw HTML. Its root_url and protocol options can provide a base for relative references. See the PDFKit README for the supported configuration.

Using Wicked PDF

Wicked PDF wraps wkhtmltopdf and is commonly used from Rails. Its README recommends absolute references for linked CSS and other assets because the executable runs outside the Rails application context. In Rails views, use the project’s stylesheet helpers; for an asset that must be embedded, the README documents wicked_pdf_asset_base64.

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

Wicked PDF does not document a dedicated CSS-string parameter. For plain CSS text, insert a <style> element in the rendered HTML:

<style>
  <%= @css_string %>
</style>

Ensure the value is trusted or safely handled before placing it in a template. In production, precompile assets used by PDF views and verify that the generated URLs are absolute and reachable. The Rails-specific asset guidance is in the Wicked PDF README.

Why Prawn is a different choice

Prawn is a pure Ruby PDF generator, not an HTML-to-PDF renderer. Its README says it is not an HTML-to-PDF generator and that its limited inline styling is not intended for rich HTML. If your source is an HTML document and you need browser-like CSS, use an HTML renderer such as Grover, PDFKit, or Wicked PDF. Choose Prawn when you want to construct the PDF directly with Ruby drawing and layout primitives.

Choosing the renderer

Renderer CSS string support documented? External-resource behavior Typical use
Grover Yes: style_tag_options: [{ content: css_string }] Use a display_url or absolute paths when relative paths cannot be resolved Chromium-based HTML and CSS to PDF
PDFKit No dedicated option shown; put CSS in a <style> element Use complete paths, or configure root_url and protocol HTML passed to the PDFKit/wkhtmltopdf pipeline
Wicked PDF No dedicated option shown; put CSS in rendered HTML Prefer absolute references; precompile Rails PDF assets Rails-oriented wkhtmltopdf integration
Prawn Not an HTML-to-PDF path You place content with Ruby APIs rather than loading an HTML stylesheet Programmatic PDF generation

The project documentation establishes these configuration differences, but it does not establish a controlled rendering-fidelity or performance benchmark. Select the engine whose HTML, CSS, asset, and deployment requirements match your application.

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.

Debugging missing CSS in the PDF

The CSS string is present but has no effect

  • Confirm that the option is nested exactly as style_tag_options: [{ content: css_string }] in Grover.
  • Check for invalid CSS by rendering a deliberately obvious rule such as body { background: red; }.
  • Inspect generated HTML for an accidental escaped style tag or a missing closing tag.
  • Check selector specificity and print rules. A later rule, an inline style, or a print stylesheet can override the declaration.

Linked styles or images disappear

  • Replace relative URLs with absolute URLs or configure Grover’s display_url.
  • For PDFKit, set an appropriate root_url and protocol, or use complete paths.
  • For Wicked PDF, verify absolute asset references and production precompilation.
  • Make sure the worker running the renderer can reach the host and read the files; your web browser’s access is not evidence that the renderer has the same access.

The PDF job hangs or fails

  • Check that the renderer’s executable and Chromium or wkhtmltopdf dependency are installed in the worker environment.
  • Remove network-dependent assets temporarily to identify a blocked request or unreachable host.
  • Log the final HTML, CSS length, resource URLs, and renderer error output (without logging secrets such as authorization headers).
  • Use a fixed timeout at the job layer and retry only failures that are safe to repeat.

Pagination differs between development and production

  • Use the same renderer version, fonts, locale, and timezone in both environments.
  • Embed or install the fonts your layout requires and avoid relying on a developer laptop’s font set.
  • Set page size and margins explicitly with @page or the renderer’s PDF options.

Performance, reliability, and cost considerations

Inline CSS avoids a file lookup, but the expensive work is still browser startup, HTML layout, font loading, image decoding, and PDF serialization. Reuse a long-lived browser process where your chosen integration supports it, keep CSS and HTML no larger than necessary, and avoid waiting for third-party resources that are not needed in the document.

For repeatable jobs, cache stable assets, pin the runtime image, and record the renderer configuration with the generated document. A cache can improve latency, but it must be invalidated when CSS, HTML, fonts, or data change. None of the cited project documentation supplies a universal throughput, fidelity, or cost benchmark, so measure with your own documents and deployment.

Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Or skip the browser setup

If the page you need is already published at a URL, ScreenshotNeo can return a screenshot or PDF through one HTTP request. It accepts the cookie or consent banner as a visitor and removes more than 60 known consent platforms, newsletter popups, and chat widgets before capture; each cleanup step can be disabled. Bot checks or CAPTCHAs, blank pages, timeouts, failed loads, and cache hits are not billed, and the response identifies the page verdict and billing status in X-Page-Verdict and X-Billed headers. An MCP server provides take_screenshot, get_page_info, and capture_pdf tools for Claude, Cursor, and other MCP clients.

See the ScreenshotNeo documentation for all options, including PDF paper size, margins, landscape mode, page ranges, custom CSS and JavaScript, waits, headers, cookies, authentication, geolocation, caching, signed links, asynchronous jobs, and bulk capture.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
curl -G "https://api.screenshotneo.com/v1/shot" 
  -d access_key=YOUR_API_KEY 
  --data-urlencode url=https://example.com 
  -o page.pdf

Use a URL that serves the HTML you want rendered. ScreenshotNeo’s free plan includes 1,000 screenshots per month with no card; paid plans start at $5 for 3,000 shots. Every feature is available on every plan. Sign up for ScreenshotNeo to get the free monthly allowance.

FAQ

Can I pass several CSS strings to Grover?

Yes. Supply multiple style-tag option hashes, each with its own content, or combine trusted rules into one string before calling Grover.new.

Should I inline CSS for every PDF?

Not necessarily. Inline text is convenient for generated or theme-specific rules. A versioned stylesheet file is easier to share across documents, provided the renderer can resolve its path or URL.

Does Prawn accept an HTML document and its stylesheet?

No. Prawn’s documented purpose is direct PDF construction in Ruby, not browser-style HTML and CSS rendering.

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

Frequently Asked Questions

Can I pass several CSS strings to Grover?

Yes. Supply multiple style-tag option hashes, each with its own content, or combine trusted rules into one string before calling Grover.new.

Should I inline CSS for every PDF?

Not necessarily. Inline text is convenient for generated or theme-specific rules. A versioned stylesheet file is easier to share across documents, provided the renderer can resolve its path or URL.

Does Prawn accept an HTML document and its stylesheet?

No. Prawn’s documented purpose is direct PDF construction in Ruby, not browser-style HTML and CSS rendering.

The Bottom Line

For Grover, the direct solution is style_tag_options: [{ content: css_string }]. Use absolute or explicitly rooted resource paths, and choose PDFKit, Wicked PDF, or Prawn only when its rendering model fits your input.

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.