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

For a normal Rails response, put the CSS string inside a <style> element in the HTML string, then return that document with render html:. A stylesheet helper links a file or URL; it does not interpret a raw CSS string. If the destination is a PDF or image, use the renderer’s inline-style option instead. Nokogiri can parse the markup, but it does not calculate browser CSS layout.

The correct implementation depends on what “rendering” means in your application: an HTTP response, an ERB template held in memory, a browser-quality PDF/image, or HTML parsing and transformation.

Return an HTML string from Rails

Build a complete document and place the CSS text in the head. Rails then sends the resulting document as text/html.

html = <<~HTML
  <!doctype html>
  <html>
    <head>
      <meta charset="utf-8">
      <style>
        body { font-family: sans-serif; margin: 2rem; }
        .notice { color: #176b3a; font-weight: 600; }
      </style>
    </head>
    <body>
      <p class="notice">Ready</p>
    </body>
  </html>
HTML

render html: html.html_safe

Why html_safe matters

Rails escapes a string passed to render html: unless the string is already marked HTML-safe. Without the final call, tags can appear as literal text rather than being interpreted by the browser.

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.

Marking the whole string safe is appropriate only when every part of it is trusted or has been safely constructed. Never concatenate raw user input into this string and then call html_safe. Escape user-provided values, or generate the document with Rails tag helpers and normal view escaping.

#1 Best Overall

Layouts are not included automatically

An inline HTML response omits the application layout by default. Pass layout: true or a named layout when that is what you want:

render html: html.html_safe, layout: true
# or
render html: html.html_safe, layout: 'print'

For a small response this is convenient. For a substantial page, a normal view template is easier to maintain than a large heredoc.

Choose the rendering path

Requirement Use Key distinction
Return a small HTML document render html: Literal HTML; ordinary strings are escaped.
Evaluate ERB held in a string render inline: Runs ERB; it is template evaluation, not simple HTML return.
Apply CSS text to browser HTML A <style> element Keep the CSS in the document head.
Generate a PDF, PNG, or JPEG Grover or another document renderer A browser engine performs layout and needs its runtime and assets.
Inspect or transform markup Nokogiri Parses a tree; it does not render visual CSS.

When the string contains ERB

If the string is a template source such as <h1>Hello, <%= @name %>!</h1>, use render inline::

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
template = '<!doctype html>
<style>.title { color: #176b3a; }</style>
<h1 class="title">Hello, <%= @name %>!</h1>'
render inline: template, layout: false

Rails evaluates the ERB expression before returning the result. Inline templates also do not use a layout unless you request one. Keep the data used by the template escaped according to its context, and prefer a view file for complex pages; the Rails guide describes inline rendering as an uncommon choice for application views.

CSS strings, linked stylesheets, and assets

Embed raw CSS

Interpolation is useful when a component or generated report supplies CSS dynamically:

css = <<~CSS
  .card { border: 1px solid #ddd; padding: 1rem; }
  .card--warning { background: #fff4cc; }
CSS

body = '<div class="card card--warning">Check this item</div>'
html = "<!doctype html><html><head><style>#{css}</style></head><body>#{body}</body></html>"
render html: html.html_safe

Only interpolate CSS that your application controls. If a user can supply the CSS, treat it as untrusted content and validate or sanitize it before including it.

Link a CSS resource

For a file managed by the Rails asset pipeline, use a normal view and stylesheet_link_tag:

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
<%= stylesheet_link_tag 'application', media: 'all' %>

The helper emits a <link> element pointing to a stylesheet resource. It is not an API for passing CSS text. A linked file is generally preferable when the same styles are reused, cacheability matters, or the stylesheet is too large for an inline response.

Generate a PDF or image with inline CSS

A Rails response only returns bytes to a browser. It does not itself create a PDF or screenshot. A browser-backed renderer such as Grover accepts HTML and can inject CSS text through style_tag_options:

style_tag_options = [
  { content: '.body { background: red; color: white; }' }
]

pdf = Grover.new(
  '<html><body class="body"><h1>Heading</h1></body></html>',
  style_tag_options: style_tag_options
).to_pdf

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

Grover uses Puppeteer and Chromium and can produce PDF, PNG, and JPEG output. The renderer must be installed and usable in the environment where the Ruby process runs. For direct calls outside middleware, plan how relative assets are resolved: provide a display_url or convert relative image, font, and stylesheet paths to absolute URLs or filesystem paths. Without a display URL, Chromium resolves relative paths against a default host, which commonly makes local assets appear missing.

PDF-specific options

Use the renderer’s documented PDF options for paper size, margins, landscape orientation, and page ranges. Keep CSS that affects pagination in the injected style, and verify print-specific rules such as @page and print-color-adjust in the Chromium version installed with your application. The available documentation demonstrates the inline-style mechanism; it does not establish universal performance or feature parity across all Chromium and Puppeteer versions.

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.

WickedPDF as another documented route

WickedPDF also accepts HTML through pdf_from_string. Its documentation shows absolute asset paths and a stylesheet helper for file-based CSS. The referenced example is for version 0.9.4, so check the version installed in your application before copying its options or assuming compatibility.

Why Nokogiri does not solve CSS rendering

Nokogiri’s HTML5 parser accepts a document with Nokogiri.HTML5(input) or a fragment with Nokogiri::HTML5.fragment(input). You can query nodes, add a style element, rewrite attributes, and serialize the result:

document = Nokogiri.HTML5('<article><h1>Title</h1></article>')
head = document.at_css('head') || document.at_css('html').add_child('<head></head>').first
head.add_child('<style>article { max-width:  fortyrem; }</style>')
puts document.to_html

That code manipulates markup only. Nokogiri does not load fonts, execute JavaScript, calculate computed styles, or paint pixels. The HTML5 API is also not available on JRuby according to its documentation. Send the resulting HTML to a browser-based renderer when you need visual output.

Security and correctness checklist

  • Keep trusted CSS and HTML separate from user-provided values.
  • Escape text inserted into HTML; do not use html_safe as a shortcut around escaping.
  • Use render inline: only when you intentionally want ERB evaluation.
  • Choose stylesheet_link_tag for a stylesheet resource, not a raw CSS string.
  • For PDF or image output, make relative assets resolvable from the renderer process.
  • Set an explicit character encoding and include a doctype for predictable browser parsing.
  • Test print layout separately from the normal web response; viewport, fonts, and pagination can change the result.

Troubleshooting common failures

The browser displays the tags instead of styling the page

The response was escaped. Confirm that the document is trusted before using html.html_safe, or render through a normal Rails template so Rails can escape individual values while preserving the template’s markup.

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

The CSS is present but has no effect

Inspect the final HTML and verify that the CSS is inside a <style> element in the document. Check selector specificity, malformed CSS, and whether a later rule overrides it. A stylesheet_link_tag pointing at a missing asset will not apply an in-memory string.

ERB appears literally

render html: returns literal markup; it does not evaluate ERB. Use render inline: for a deliberately evaluated template string, or move the template to a view file.

The PDF has no images, fonts, or styles

Relative URLs are often the cause when Grover is called directly. Supply a suitable display_url, use absolute URLs, or provide filesystem paths that Chromium can read. Also confirm that the Chromium/Puppeteer runtime is installed and that the process has network or file permissions.

The output is an HTML tree but not a visual page

Nokogiri is a parser, not a layout engine. Serialize its result and pass it to a browser renderer for PDF, PNG, or JPEG output.

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

The response unexpectedly lacks the application layout

Inline HTML rendering does not include a layout by default. Add layout: true or specify the layout name.

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 your goal is a reliable screenshot or PDF endpoint rather than building and operating a Chromium pipeline, ScreenshotNeo accepts one request with a URL. It removes cookie-consent banners, newsletter popups, and chat widgets before capture; bot checks, blank pages, failed loads, timeouts, and cache hits are not billed. Every response identifies the page verdict and billing status in X-Page-Verdict and X-Billed headers. It also provides an MCP server with take_screenshot, get_page_info, and capture_pdf tools for Claude, Cursor, and other MCP clients.

See the ScreenshotNeo API documentation for authentication and options. This cURL request captures the target URL as an image:

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

The same endpoint can be called from Ruby, Python, or Node.js:

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
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')
File.binwrite('shot.webp', Net::HTTP.get(uri))
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}`);

ScreenshotNeo supports PNG, JPEG, WebP, and PDF output plus full-page capture, lazy-image loading, CSS-selector element capture, dark mode, device presets, custom viewports, retina scale, custom CSS and JavaScript, waits, request blocking, headers, cookies, user agents, authorization, timezone, geolocation, transparent backgrounds, resizing, configurable caching, signed links, asynchronous jobs, webhooks, bulk capture of up to 100 URLs per call, a usage API, and an OpenAPI specification. Its parameter names are compatible with those used by many screenshot APIs, which can reduce migration work.

There is a free allowance of 1,000 screenshots per month with no card. Paid plans start at $5 for 3,000 screenshots; yearly billing provides two months free. Create a free ScreenshotNeo account to try the endpoint.

Performance, reliability, and cost choices

Inline HTML versus linked CSS

Inlining a small stylesheet removes a separate asset request and makes an HTML fragment self-contained, which is useful for email-like snippets and generated documents. Large or shared stylesheets are easier to cache and maintain as linked assets. The right choice depends on payload size, reuse, and whether the consumer can fetch external resources.

Browser rendering versus parsing

Browser-backed rendering performs layout and painting, so it requires more runtime resources than string assembly or Nokogiri parsing. Parsing is appropriate for structural edits; it cannot replace a visual renderer. Do not infer a speed or memory advantage for one library without testing the exact versions, document size, assets, and deployment environment.

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

Caching generated captures

For repeated URLs, cache deliberately and define an invalidation policy. ScreenshotNeo lets you choose a cache TTL and reports cache hits as not billed. For an in-process Grover pipeline, cache the finished artifact only when the underlying page and CSS are known to be unchanged.

Frequently Asked Questions

Can I pass a CSS string directly to `stylesheet_link_tag`?

No. Use a `

Recommended PC Tool
Recommended PC Tool

Two free Windows tools

One Free Minute Could Fix That PC

Before you go - each of these free tools takes about a minute and tackles what quietly slows a Windows PC down.

Special offer. View Outbyte info, uninstall instructions, EULA, and Privacy Policy.