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

Some links on this page are affiliate links: if you buy through them we may earn a commission, at no extra cost to you.

With Grover, pass your CSS string as the content of a style_tag_options entry. Chromium then receives a normal <style> element before Grover creates the PDF:

css = '.body { background: red; }'
html = '<html><body><h1>Heading</h1></body></html>'
pdf = Grover.new(
  html,
  style_tag_options: [{ content: css }]
).to_pdf

This is different from loading a stylesheet file by path or URL. The right implementation depends on the Ruby PDF renderer you use, how it resolves assets, and whether you are rendering HTML at all.

Pass a CSS string to Grover with style_tag_options

Grover documents inline HTML input and a style_tag_options option whose content value is CSS text. A complete example writes the returned PDF to disk:

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

html = <<~HTML
  <!doctype html>
  <html>
    <head>
      <meta charset="utf-8">
      <title>Invoice</title>
    </head>
    <body class="body">
      <h1>Invoice 1001</h1>
      <p class="total">$125.00</p>
    </body>
  </html>
HTML

css = <<~CSS
  @page { size: A4; margin: 18mm; }
  .body { font-family: Arial, sans-serif; color: #222; }
  h1 { color: #1457a6; margin-bottom: 8mm; }
  .total { font-size: 20px; font-weight: 700; }
CSS

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

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

The resulting bytes are the PDF, so use binary file I/O. In a Rails controller, you can return them with send_data pdf, filename: 'invoice.pdf', type: 'application/pdf', disposition: 'inline'.

#1 Best Overall

Keep the CSS separate from HTML generation

Build the HTML and CSS independently, then inject the CSS through Grover’s supported option. This keeps templates readable and lets you test the CSS string before rendering. Do not confuse content with url or path: content is for CSS text, while the other forms are for separately stored stylesheets.

Use multiple style blocks when useful

Grover accepts an array, so you can provide a base sheet and a print-specific sheet:

Grover.new(
  html,
  style_tag_options: [
    { content: base_css },
    { content: print_css }
  ]
).to_pdf

Order matters as it does in HTML. Later rules can override earlier rules when selector specificity and other cascade conditions are equal.

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.

Make external assets resolvable

Inline CSS solves the stylesheet-string problem, but CSS often references fonts, images, or additional files. A renderer must know what a relative URL means.

Grover: set a display URL or use absolute paths

Grover’s documentation warns that direct conversions need a display_url or absolute paths for relative resources. Without one, Chromium resolves relative paths against its default display URL, http://example.com. For a local HTML document, configure a base URL that points to the directory or host containing the assets, or change references to resolvable absolute URLs. See the Grover README for the documented options.

pdf = Grover.new(
  html,
  display_url: 'https://www.example.com/orders/1001',
  style_tag_options: [{ content: css }]
).to_pdf

Only use a URL that the rendering process can actually reach. A browser running in a container, worker, or restricted network may not be able to access your development machine or private host.

Fonts and images in CSS

For background-image, @font-face, and image URLs, verify the URL from the renderer’s environment, not only from your laptop’s browser. If an asset is private, provide an accessible route or use an embedding strategy appropriate to your application. A CSS string can be valid while its referenced resources still fail to load.

What’s actually slowing this PC down?

Pick the symptom - the matching free tool is one click away.

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

PDFKit: put the string in a <style> element

PDFKit’s README documents PDFKit.new(html) and adding stylesheet file paths with kit.stylesheets << '/path/to/css/file'; it does not document a dedicated CSS-string parameter. When your stylesheet already exists as a string, insert it into the HTML passed to PDFKit:

require 'pdfkit'

css = '.body { background: #f4f6f8; }'
html = <<~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)

When using file-based stylesheets instead, give PDFKit complete paths for CSS, images, and JavaScript in raw HTML. Its root_url and protocol options can help resolve relative references. Consult the PDFKit README for the supported configuration.

Wicked PDF: embed the style in rendered HTML or use asset helpers

Wicked PDF runs wkhtmltopdf. Its Rails-oriented documentation recommends absolute references because the executable runs outside the Rails application context. For a CSS string, a normal HTML-level approach is a <style> element in the view or generated markup:

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

<h1><%= @title %></h1>

For linked assets, follow Wicked PDF’s documented stylesheet helpers and, where appropriate, wicked_pdf_asset_base64. Precompile assets used by PDF views and check production paths; development asset resolution can hide deployment-only failures. The project documentation is at the Wicked PDF README.

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

When Prawn is the wrong tool for this job

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 unsuitable for rich HTML. If you need CSS selectors, normal HTML layout, or a CSS string applied to an HTML document, choose an HTML-capable renderer such as Grover, PDFKit, or Wicked PDF. Use Prawn when you intend to construct the PDF directly with Ruby drawing and text APIs. See the Prawn project README.

Renderer comparison

Renderer CSS string documented directly? Resource resolution Runtime and fit
Grover Yes: style_tag_options: [{ content: css_string }] Use display_url or absolute paths for relative resources Puppeteer and Chromium; HTML-to-PDF rendering
PDFKit No dedicated string option shown; inject a <style> element Complete paths; root_url and protocol can resolve relatives HTML-to-PDF workflow documented by the project
Wicked PDF No dedicated string option shown; put CSS in rendered HTML Absolute references, compiled assets, or documented helpers wkhtmltopdf-based, Rails-oriented integration
Prawn Not applicable to HTML CSS You place content through Ruby APIs Pure Ruby PDF generation, not HTML-to-PDF

The project documentation establishes these configuration differences, but it does not establish a controlled benchmark or a universal CSS-fidelity ranking.

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

Debug missing styles systematically

  1. Confirm the CSS reaches the renderer. Log or inspect the string immediately before creating the PDF. Check for an empty value, truncated template output, or interpolation that produced invalid CSS.
  2. Check selector and markup agreement. A rule for .invoice-total cannot style an element whose class is total. Inspect the generated HTML, not the source template alone.
  3. Check resource URLs. Replace relative URLs temporarily with absolute, reachable URLs, or configure Grover’s display_url, PDFKit’s root_url/protocol, or Wicked PDF’s documented asset approach.
  4. Check renderer-specific CSS support. A browser-based renderer and wkhtmltopdf may differ in support for newer layout or print features. Reduce the case to one element and one rule, then add complexity back gradually.
  5. Check print rules and page settings. Rules inside @media print, page margins, and @page declarations can change the output even when ordinary screen styling looks correct.
  6. Check deployment dependencies. Grover needs its Puppeteer/Chromium setup; Wicked PDF needs a working wkhtmltopdf executable; PDFKit also depends on its underlying command-line renderer. A missing executable can look like a CSS failure but occurs before styling is applied.

Common symptoms and fixes

  • Everything is unstyled: verify the Grover option name and nesting, or verify that PDFKit/Wicked PDF actually received a <style> element.
  • Colors work but images or fonts do not: fix URL resolution and network/file permissions; the CSS itself may already be loaded.
  • Works locally, fails in production: use absolute or container-reachable paths and precompile Rails assets used by PDF views.
  • PDF is blank or conversion times out: inspect HTML validity, JavaScript waits, renderer installation, and access to every remote resource before tuning CSS.

Or skip the browser setup

If your actual deliverable is a clean screenshot of a rendered page rather than a Ruby-generated PDF, ScreenshotNeo provides a one-request capture API. It accepts cookie and consent banners as a visitor and removes more than 60 known consent platforms, newsletter popups, and chat widgets before capture; each step can be turned off. Bot checks or CAPTCHAs, blank pages, timeouts, failed loads, and cache hits are not billed, and the response identifies the result with X-Page-Verdict and X-Billed headers. Its MCP server exposes take_screenshot, get_page_info, and capture_pdf to Claude, Cursor, and other MCP clients.

cURL:

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

Ruby:

require 'net/http'
require 'uri'

uri = URI('https://api.screenshotneo.com/v1/shot')
uri.query = URI.encode_www_form(access_key: 'YOUR_API_KEY', url: 'https://stripe.com')
response = Net::HTTP.get_response(uri)
File.binwrite('shot.webp', response.body)

Python:

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)

Node.js:

const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);

See the ScreenshotNeo documentation for options such as full-page capture, CSS-selector element capture, device and retina settings, custom CSS and JavaScript, waits, request blocking, headers, cookies, geolocation, transparent backgrounds, resizing, caching, signed links, asynchronous webhooks, bulk capture, usage data, and the OpenAPI specification. The free plan includes 1,000 screenshots per month with no card; paid plans start at $5 for 3,000. Create a free ScreenshotNeo account.

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

FAQ

Can I pass a CSS string to Grover’s url option?

No. Use style_tag_options with a content value for CSS text. Use url or path for a separately stored stylesheet.

Should I escape CSS before inserting it?

Keep the string valid CSS and ensure any HTML interpolation is safely handled by your templating layer. Do not insert untrusted text into generated HTML without the same sanitization precautions you use elsewhere.

Why does a CSS file load in a browser but not in a PDF job?

The PDF process may run in another container, lack authentication, or have no base URL for relative references. Test the exact URL and filesystem path from the renderer’s runtime.

Is Prawn able to render my existing HTML and stylesheet?

Not as a drop-in HTML renderer. Prawn is intended for constructing PDFs with Ruby code rather than applying CSS to an HTML document.

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.