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-inlineThe 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.htmlFor full details of the options available, you can use the --help flag:
css-inline --helpIf 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.