css_inline
is a high-performance library for inlining CSS into HTML 'style' attributes.
This library is designed for scenarios such as preparing HTML emails or embedding HTML into third-party web pages.
For instance, the crate transforms HTML like this:
<html> <head><style>h1 { color:blue; }</style> </head> <body><h1>Big Text</h1> </body></html>
into:
<html> <head></head> <body><h1 style="color:blue;">Big Text</h1> </body></html>
Uses reliable components from Mozilla's Servo project
Inlines CSS from style
and link
tags
Removes style
and link
tags
Resolves external stylesheets (including local files)
Optionally caches external stylesheets
Works on Linux, Windows, and macOS
Supports HTML5 & CSS3
Bindings for Python, Ruby, JavaScript, C, and a WebAssembly module to run in browsers.
Command Line Interface
If you'd like to try css-inline
, you can check the WebAssembly-powered playground to see the results instantly.
To include it in your project, add the following line to the dependencies section in your project's Cargo.toml
file:
[dependencies]css-inline = "0.14"
The Minimum Supported Rust Version is 1.65.
const HTML: &str = r#"<html><head> <style>h1 { color:blue; }</style></head><body> <h1>Big Text</h1></body></html>"#;fn main() -> css_inline::Result<()> {let inlined = css_inline::inline(HTML)?;// Do something with inlined HTML, e.g. send an emailOk(())}
Note that css-inline
automatically adds missing html
and body
tags, so the output is a valid HTML document.
Alternatively, you can inline CSS into an HTML fragment, preserving the original structure:
const FRAGMENT: &str = r#"<main><h1>Hello</h1><section><p>who am i</p></section></main>"#;const CSS: &str = r#"p { color: red;}h1 { color: blue;}"#;fn main() -> css_inline::Result<()> {let inlined = css_inline::inline_fragment(FRAGMENT, CSS)?;Ok(())}
css-inline
can be configured by using CSSInliner::options()
that implements the Builder pattern:
const HTML: &str = "...";fn main() -> css_inline::Result<()> {let inliner = css_inline::CSSInliner::options().load_remote_stylesheets(false).build();let inlined = inliner.inline(HTML)?;// Do something with inlined HTML, e.g. send an emailOk(())}
inline_style_tags
. Specifies whether to inline CSS from "style" tags. Default: true
keep_style_tags
. Specifies whether to keep "style" tags after inlining. Default: false
keep_link_tags
. Specifies whether to keep "link" tags after inlining. Default: false
base_url
. The base URL used to resolve relative URLs. If you'd like to load stylesheets from your filesystem, use the file://
scheme. Default: None
load_remote_stylesheets
. Specifies whether remote stylesheets should be loaded. Default: true
cache
. Specifies cache for external stylesheets. Default: None
extra_css
. Extra CSS to be inlined. Default: None
preallocate_node_capacity
. Advanced. Preallocates capacity for HTML nodes during parsing. This can improve performance when you have an estimate of the number of nodes in your HTML document. Default: 32
You can also skip CSS inlining for an HTML tag by adding the data-css-inline="ignore"
attribute to it:
<head> <style>h1 { color:blue; }</style></head><body> <!-- The tag below won't receive additional styles --> <h1 data-css-inline="ignore">Big Text</h1></body>
The data-css-inline="ignore"
attribute also allows you to skip link
and style
tags:
<head> <!-- Styles below are ignored --> <style data-css-inline="ignore">h1 { color:blue; }</style></head><body> <h1>Big Text</h1></body>
Alternatively, you may keep style
from being removed by using the data-css-inline="keep"
attribute.
This is useful if you want to keep @media
queries for responsive emails in separate style
tags:
<head> <!-- Styles below are not removed --> <style data-css-inline="keep">h1 { color:blue; }</style></head><body> <h1>Big Text</h1></body>
Such tags will be kept in the resulting HTML even if the keep_style_tags
option is set to false
.
If you'd like to load stylesheets from your filesystem, use the file://
scheme:
const HTML: &str = "...";fn main() -> css_inline::Result<()> {let base_url = css_inline::Url::parse("file://styles/email/").expect("Invalid URL");let inliner = css_inline::CSSInliner::options().base_url(Some(base_url)).build();let inlined = inliner.inline(HTML);// Do something with inlined HTML, e.g. send an emailOk(())}
For resolving remote stylesheets it is possible to implement a custom resolver:
#[derive(Debug, Default)]pub struct CustomStylesheetResolver;impl css_inline::StylesheetResolver for CustomStylesheetResolver {fn retrieve(&self, location: &str) -> css_inline::Result<String> {Err(self.unsupported("External stylesheets are not supported"))}}fn main() -> css_inline::Result<()> {let inliner = css_inline::CSSInliner::options().resolver(std::sync::Arc::new(CustomStylesheetResolver)).build();Ok(())}
You can also cache external stylesheets to avoid excessive network requests:
use std::num::NonZeroUsize;#[cfg(feature = "stylesheet-cache")]fn main() -> css_inline::Result<()> {let inliner = css_inline::CSSInliner::options().cache(// This is an LRU cachecss_inline::StylesheetCache::new(NonZeroUsize::new(5).expect("Invalid cache size"))).build();Ok(())}// This block is here for testing purposes#[cfg(not(feature = "stylesheet-cache"))]fn main() -> css_inline::Result<()> {Ok(())}
Caching is disabled by default.
css-inline
typically inlines HTML emails within hundreds of microseconds, though results may vary with input complexity.
Benchmarks for css-inline==0.14.1
:
Basic: 6.44 µs, 230 bytes
Realistic-1: 128.59 µs, 8.58 KB
Realistic-2: 81.44 µs, 4.3 KB
GitHub page: 224.89 ms, 1.81 MB
These benchmarks, conducted using rustc 1.78
on M1 Max, can be found in css-inline/benches/inliner.rs
.
Install with cargo
:
cargo install css-inline
The following command inlines CSS in multiple documents in parallel. The resulting files will be saved
as inlined.email1.html
and inlined.email2.html
:
css-inline email1.html email2.html
For full details of the options available, you can use the --help
flag:
css-inline --help
If you're interested in learning how this library was created and how it works internally, check out these articles:
Rust crate
Python bindings
If you have any questions or discussions related to this library, please join our gitter!
This project is licensed under the terms of the MIT license.