css_inline
css_inline is a high-performance library for inlining CSS into HTML 'style' attributes.
Use it to prepare HTML emails or to embed HTML into third-party web pages.
It turns this HTML:
<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 components from Mozilla's Servo project
- 14x to 680x faster than
premailer, the fastest Python alternative (see Performance) - Inlines CSS from
styleandlinktags - Removes
styleandlinktags - Resolves external stylesheets (including local files)
- Caches external stylesheets (opt-in)
- Processes multiple documents in parallel
- Works on Linux, Windows, macOS and in the browser via Pyodide
- Supports HTML5 & CSS3
- Tested on CPython 3.10, 3.11, 3.12, 3.13, 3.14 and PyPy 3.11.
Playground
Try css-inline in the WebAssembly-powered playground.
Installation
Install with pip:
pip install css_inline
The PyPI name is css-inline; pip accepts css_inline too. Import it as css_inline.
PyPI ships pre-compiled wheels for:
- Linux (glibc and musl): x86_64, aarch64, armv7; glibc also i686
- macOS: x86_64, arm64
- Windows: x64, x86
- PyPy 3.11: Linux x86_64 and aarch64, macOS x86_64
On other platforms, pip builds the package from source, which requires Rust 1.85 or newer.
Usage
import css_inline
HTML = """<html>
<head>
<style>h1 { color:blue; }</style>
</head>
<body>
<h1>Big Text</h1>
</body>
</html>"""
inlined = css_inline.inline(HTML)
# <html><head>
#
# </head>
# <body>
# <h1 style="color: blue;">Big Text</h1>
#
# </body></html>
inline adds missing html, head and body tags, so the output is a complete HTML document.
To inline CSS into an HTML fragment, use inline_fragment(html, css). Both arguments are required; css holds the stylesheet to apply. The output stays a fragment: no <html>, <head> or <body> wrapper. Use inline for full documents:
FRAGMENT = """<main>
<h1>Hello</h1>
<section>
<p>who am i</p>
</section>
</main>"""
CSS = """
p {
color: red;
}
h1 {
color: blue;
}
"""
inlined = css_inline.inline_fragment(FRAGMENT, CSS)
# <main>
# <h1 style="color: blue;">Hello</h1>
# <section>
# <p style="color: red;">who am i</p>
# </section>
# </main>
To inline many documents at once, use inline_many or inline_many_fragments.
Both process inputs in parallel and return a list of strings in input order.
inline_many_fragments pairs htmls[i] with css[i].
Warning: if the lists differ in length, inline_many_fragments silently drops the extra items from the longer list.
CSSInliner has the same two methods.
import css_inline
css_inline.inline_many(["<...>", "<...>"])
css_inline.inline_many_fragments(["<p>a</p>", "<p>b</p>"], ["p { color: red; }", "p { color: blue; }"])
inline_many processes inputs in parallel on a Rust thread pool (Rayon), so you don't need multiprocessing.
The speedup scales with the number of CPU cores.
Configuration
Pass options to the CSSInliner class:
import css_inline
inliner = css_inline.CSSInliner(keep_style_tags=True)
inliner.inline("...")
inline_style_tags. Inline CSS fromstyletags. Default:Truekeep_style_tags. Keepstyletags after inlining. Default:Falsekeep_link_tags. Keeplinktags after inlining. Default:Falsekeep_at_rules. Keep at-rules (starting with@) after inlining. Default:Falseminify_css. Remove trailing semicolons and spaces between properties and values. Default:Falsebase_url. Base URL for resolving relative URLs. Use thefile://scheme to load stylesheets from the filesystem. Default:Noneload_remote_stylesheets. Load stylesheets fromlinktags, over the network or fromfile://paths. Default:Truecache. Cache for external stylesheets, for exampleStylesheetCache(size=5). Default:Noneextra_css. Extra CSS to inline. Default:Nonepreallocate_node_capacity. Advanced. Number of HTML nodes to preallocate during parsing. Set it to your expected node count to avoid reallocations. Default:32remove_inlined_selectors. Remove inlined selectors from<style>blocks. Default:Falseapply_width_attributes. AddwidthHTML attributes from CSSwidthproperties on supported elements (table,td,th,img). Default:Falseapply_height_attributes. AddheightHTML attributes from CSSheightproperties on supported elements (table,td,th,img). Default:False
To skip CSS inlining for an HTML tag, add the data-css-inline="ignore" attribute:
<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 same attribute on a link or style tag makes the inliner skip that stylesheet:
<head>
<!-- Styles below are ignored -->
<style data-css-inline="ignore">h1 { color:blue; }</style>
</head>
<body>
<h1>Big Text</h1>
</body>
To keep a style tag in the output, add data-css-inline="keep".
Use it for @media queries in responsive emails.
The tag stays even with keep_style_tags=False.
<head>
<!-- Styles below are not removed -->
<style data-css-inline="keep">h1 { color:blue; }</style>
</head>
<body>
<h1>Big Text</h1>
</body>
At-rules can't go into style attributes, so the inliner removes them by default.
Set keep_at_rules=True to keep them, for example @media queries for responsive emails, in a style tag while inlining everything else.
The tag stays even with keep_style_tags=False.
<head>
<!-- With keep_at_rules=True, "color: blue" goes into <h1> and @media stays in <style> -->
<style>h1 { color: blue; } @media (max-width: 600px) { h1 { font-size: 18px; } }</style>
</head>
<body>
<h1>Big Text</h1>
</body>
minify_css=True removes trailing semicolons and spaces between properties and values in inlined styles.
<head>
<!-- With minify_css=True, the <h1> will have `style="color:blue;font-weight:bold"` -->
<style>h1 { color: blue; font-weight: bold; }</style>
</head>
<body>
<h1>Big Text</h1>
</body>
To load stylesheets from the filesystem, use the file:// scheme.
Standard URL parsing reads file://styles/email/ as host styles and path /email/.
css-inline deviates from the standard here: for a file:// URL with a host, it drops the file:// prefix and treats styles/email/ as a path relative to the current directory.
With either form, <link href="main.css"> resolves against the base, so keep the trailing slash: without it, main.css replaces the last segment (styles/main.css).
import css_inline
# Absolute directory: three slashes
inliner = css_inline.CSSInliner(base_url="file:///srv/app/styles/email/")
inliner.inline("...")
# Relative to the current directory: two slashes
inliner = css_inline.CSSInliner(base_url="file://styles/email/")
inliner.inline("...")
To avoid repeated network requests, cache external stylesheets:
import css_inline
inliner = css_inline.CSSInliner(
cache=css_inline.StylesheetCache(size=5)
)
inliner.inline("...")
size is the maximum number of stylesheets in the cache and must be greater than zero; otherwise StylesheetCache raises InlineError. When it is full, the least recently used entry is evicted. Caching is off by default.
Errors
Inlining raises css_inline.InlineError, a ValueError subclass, when a stylesheet fails to load (missing file, network error) or to parse.
An invalid base_url raises a plain ValueError.
import css_inline
try:
css_inline.inline('<link href="missing.css" rel="stylesheet">')
except css_inline.InlineError as exc:
print(exc) # Missing stylesheet file: missing.css
XHTML compatibility
css-inline outputs HTML5, so void tags stay unclosed (<hr>, not <hr/>). For XHTML output, re-serialize with lxml:
import css_inline
from lxml import html, etree
document = "..." # Your HTML document
inlined = css_inline.inline(document)
tree = html.fromstring(inlined)
inlined = etree.tostring(tree).decode(encoding="utf-8")
Performance
css-inline is 14x to 680x faster than the next fastest Python inliner, premailer, on the inputs below:
| Size | css_inline 0.22.0 |
premailer 3.10.0 |
toronado 0.1.0 |
pynliner 0.8.0 |
|
|---|---|---|---|---|---|
| Basic | 230 B | 4.19 µs | 90.77 µs (21.66x) | 538.65 µs (128.56x) | 951.89 µs (227.18x) |
| Realistic-1 | 8.58 KB | 78.15 µs | 1.12 ms (14.32x) | 12.25 ms (156.77x) | 12.92 ms (165.31x) |
| Realistic-2 | 4.30 KB | 42.70 µs | 1.47 ms (34.38x) | ERROR | ERROR |
| GitHub page | 1.81 MB | 16.79 ms | 11.42 s (680.16x) | ERROR | ERROR |
toronado and pynliner fail on Realistic-2 and the GitHub page because those inputs contain CSS they do not support: toronado raises ExpressionError: Pseudo-elements are not supported., pynliner raises Exception: No match was found. We're done or something is broken.
inlinestyler 0.2.5 is not in the table: it crashes with current lxml ('CSSSelector' object has no attribute 'evaluate').
Inputs live in benchmarks/benchmarks.json: Basic is simple, Realistic-1 is big_email_1, Realistic-2 is big_email_2, GitHub page is big_page.
Benchmark code: benches/bench.py (pytest-benchmark, mean time). Environment: rustc 1.99 (stable), Python 3.14.5, Ryzen 9 9950X.
Comparison with other libraries
Compared to other Python inliners, css-inline:
- Supports more CSS features. For example,
toronadoandpynlinerdon't support pseudo-elements. - Has fewer configuration options than
premailer. - Has no debug logs.
- Supports only HTML5.
Further reading
How the library works internally:
License
This project is licensed under the terms of the MIT license.
Metadata
Release files for css-inline 0.22.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| css_inline-0.22.1.tar.gz | 76.6 kB | Details |
Built distributions (wheels)
Total release size: 29.5 MB
Release files / css_inline-0.22.1.tar.gz
| Download URL | css_inline-0.22.1.tar.gz |
|---|---|
| Size | 76.6 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
603fcc4b88a1337adc02761ca185c420dfc9a11865694c602f39f38b0f40812e
|
|
BLAKE2b-256 checksum How to use checksums |
3f38eb931ad7ed5b53b12615d41d9577cf374f8642102c06432050ed16786844
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.10.12
|
Release files / css_inline-0.22.1-pp311-pypy311_pp73-manylinux_2_24_x86_64.whl
| Download URL | css_inline-0.22.1-pp311-pypy311_pp73-manylinux_2_24_x86_64.whl |
|---|---|
| Size | 2.0 MB |
| Tags | Linux glibc 2.24+ x86-64 PyPy 3.11 PyPy 3.11 7.3 |
|
SHA-256 checksum How to use checksums |
078f78bd8f37e535fa698dd65972d08a32d87721a33c81b8cf8dea6a3e8aa977
|
|
BLAKE2b-256 checksum How to use checksums |
b6fe664d1470c423c3a5f3c4d6ca3178fbac3857bca7ea55a7af91130ff2cdeb
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.10.12
|
Release files / css_inline-0.22.1-pp311-pypy311_pp73-manylinux_2_24_aarch64.whl
| Download URL | css_inline-0.22.1-pp311-pypy311_pp73-manylinux_2_24_aarch64.whl |
|---|---|
| Size | 1.9 MB |
| Tags | Linux glibc 2.24+ ARM64 PyPy 3.11 PyPy 3.11 7.3 |
|
SHA-256 checksum How to use checksums |
0d0e2f0c8a06ffb04556d6b5f5d08840ce04036114acf6c91d1b005204bab53a
|
|
BLAKE2b-256 checksum How to use checksums |
75a7dda7fb4971e3ecc6fad67fd813c7367a9fed6bed51de8177223926f7a3ea
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.10.12
|
Release files / css_inline-0.22.1-pp311-pypy311_pp73-macosx_10_12_x86_64.whl
| Download URL | css_inline-0.22.1-pp311-pypy311_pp73-macosx_10_12_x86_64.whl |
|---|---|
| Size | 1.8 MB |
| Tags | PyPy 3.11 PyPy 3.11 7.3 macOS 10.12+ x86-64 |
|
SHA-256 checksum How to use checksums |
30ee4946f68e527912b4df27a9ab5593c3e40bc18be133362a85e46b6477e052
|
|
BLAKE2b-256 checksum How to use checksums |
b93c09fad102edda227738483b7152c9a0455daaac2be825acdda679659ea304
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.10.12
|
Release files / css_inline-0.22.1-cp310-abi3-win_amd64.whl
| Download URL | css_inline-0.22.1-cp310-abi3-win_amd64.whl |
|---|---|
| Size | 1.9 MB |
| Tags | CPython 3.10 Windows x86-64 abi3 |
|
SHA-256 checksum How to use checksums |
80fd1e6030f84c62f2fc2e803e9c55a2bfb7410b36ea0bf3fee5fda48bd71d7c
|
|
BLAKE2b-256 checksum How to use checksums |
86a1d0c5c9327b8dd1af774096a135473364f8ef1a39b66f21e8fb0f19902cd4
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.10.12
|
Release files / css_inline-0.22.1-cp310-abi3-win32.whl
| Download URL | css_inline-0.22.1-cp310-abi3-win32.whl |
|---|---|
| Size | 1.6 MB |
| Tags | CPython 3.10 Windows x86-32 abi3 |
|
SHA-256 checksum How to use checksums |
94286c9ab21a572aa3c811f62e71f321d41b0a8443dabc22b398e1badf4223ea
|
|
BLAKE2b-256 checksum How to use checksums |
a4dad510355a4de202a65b89f2967dc529c9cf796152daf26cf44ba9eed8c970
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.10.12
|
Release files / css_inline-0.22.1-cp310-abi3-pyemscripten_2026_0_wasm32.whl
| Download URL | css_inline-0.22.1-cp310-abi3-pyemscripten_2026_0_wasm32.whl |
|---|---|
| Size | 524.1 kB |
| Tags | CPython 3.10 PyEmscripten 2026.0+ WebAssembly abi3 |
|
SHA-256 checksum How to use checksums |
4c21878ddb9ce14546011de5b92f34c7f0e7822fb0cf1062ca26935ae9669eab
|
|
BLAKE2b-256 checksum How to use checksums |
3e2ed7f8bc371af8fc4a5a29239711e3e5804b364be6251a91f050e746bc9074
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.10.12
|
Release files / css_inline-0.22.1-cp310-abi3-pyemscripten_2025_0_wasm32.whl
| Download URL | css_inline-0.22.1-cp310-abi3-pyemscripten_2025_0_wasm32.whl |
|---|---|
| Size | 527.4 kB |
| Tags | CPython 3.10 PyEmscripten 2025.0+ WebAssembly abi3 |
|
SHA-256 checksum How to use checksums |
dc8b185cc2ab25f16bc46348e03a76ff89ac84fbb38e0fdbc32cdc5fd926fea1
|
|
BLAKE2b-256 checksum How to use checksums |
c58660a84c12c57a5e6509eec4009fac6614cbede99a1489a40405f180f7db09
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.10.12
|
Release files / css_inline-0.22.1-cp310-abi3-musllinux_1_2_x86_64.whl
| Download URL | css_inline-0.22.1-cp310-abi3-musllinux_1_2_x86_64.whl |
|---|---|
| Size | 2.2 MB |
| Tags | CPython 3.10 Linux musl 1.2+ x86-64 abi3 |
|
SHA-256 checksum How to use checksums |
2fc866fbe9baf78394e40a8ca9364443e51ed669f5a6f91cdb44f8736e9c24f4
|
|
BLAKE2b-256 checksum How to use checksums |
67c1acada405f161900dde2267e4a49a5c5a16e779f1e1361417fddf642120a9
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.10.12
|
Release files / css_inline-0.22.1-cp310-abi3-musllinux_1_2_armv7l.whl
| Download URL | css_inline-0.22.1-cp310-abi3-musllinux_1_2_armv7l.whl |
|---|---|
| Size | 2.0 MB |
| Tags | CPython 3.10 Linux musl 1.2+ ARMv7l abi3 |
|
SHA-256 checksum How to use checksums |
69f73298810eafb031e83743967b9ca70cdfeaf28fb54398e6ff09a2c7cf05d4
|
|
BLAKE2b-256 checksum How to use checksums |
1b3a559422190a3ec58563b0906ca86e44ffbca3331c3d8d5c94fa538544bb7c
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.10.12
|
Release files / css_inline-0.22.1-cp310-abi3-musllinux_1_2_aarch64.whl
| Download URL | css_inline-0.22.1-cp310-abi3-musllinux_1_2_aarch64.whl |
|---|---|
| Size | 2.1 MB |
| Tags | CPython 3.10 Linux musl 1.2+ ARM64 abi3 |
|
SHA-256 checksum How to use checksums |
aac50f7299f194af8ae64c7180b8809ff8a27de2b2dc7e4782409626fcdbd897
|
|
BLAKE2b-256 checksum How to use checksums |
291d9cf25045f844d80c26fd02f2350a19c018e3701c4a803b02ac43afa38c72
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.10.12
|
Release files / css_inline-0.22.1-cp310-abi3-manylinux_2_24_armv7l.whl
| Download URL | css_inline-0.22.1-cp310-abi3-manylinux_2_24_armv7l.whl |
|---|---|
| Size | 1.8 MB |
| Tags | CPython 3.10 Linux glibc 2.24+ ARMv7l abi3 |
|
SHA-256 checksum How to use checksums |
6bdf12dde4175cd1ce8b87b4711893805a03b861b14a624820dd1ab046144d91
|
|
BLAKE2b-256 checksum How to use checksums |
e7bec8c818440c28970b2079331fc8cdc1d0bfb4f473e099b6114aabf7b62cc5
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.10.12
|
Release files / css_inline-0.22.1-cp310-abi3-manylinux_2_24_aarch64.whl
| Download URL | css_inline-0.22.1-cp310-abi3-manylinux_2_24_aarch64.whl |
|---|---|
| Size | 1.9 MB |
| Tags | CPython 3.10 Linux glibc 2.24+ ARM64 abi3 |
|
SHA-256 checksum How to use checksums |
5bfcc109810b4c2d3ffb711c3400bca4b1a1f0df789e65bbe01b1ea72098f423
|
|
BLAKE2b-256 checksum How to use checksums |
3bbed313521fd8d6c2169238fb48da8983084ecc4b6cc745190b1423532e4198
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.10.12
|
Release files / css_inline-0.22.1-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
| Download URL | css_inline-0.22.1-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl |
|---|---|
| Size | 2.0 MB |
| Tags | CPython 3.10 Linux glibc 2.17+ x86-64 abi3 |
|
SHA-256 checksum How to use checksums |
b0007817d1a55250ce7d8225c3ca2cd1c85905729938707c4b3af9603e8a109c
|
|
BLAKE2b-256 checksum How to use checksums |
537f4f385cbf73c8191ad2956130c5b4335a8cd8f1c476c6520f38f4c02f031c
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.10.12
|
Release files / css_inline-0.22.1-cp310-abi3-manylinux_2_12_i686.manylinux2010_i686.whl
| Download URL | css_inline-0.22.1-cp310-abi3-manylinux_2_12_i686.manylinux2010_i686.whl |
|---|---|
| Size | 1.9 MB |
| Tags | CPython 3.10 Linux glibc 2.12+ x86-32 abi3 |
|
SHA-256 checksum How to use checksums |
f2af1d0fd3bfafeb3b25a66df950f2b9d6c508bd658133b2d73633edbb4dd7f1
|
|
BLAKE2b-256 checksum How to use checksums |
640a413f54e7fc7746d138913654ef5246530372228dd338da0b83e58c615135
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.10.12
|
Release files / css_inline-0.22.1-cp310-abi3-macosx_10_12_x86_64.whl
| Download URL | css_inline-0.22.1-cp310-abi3-macosx_10_12_x86_64.whl |
|---|---|
| Size | 1.8 MB |
| Tags | CPython 3.10 abi3 macOS 10.12+ x86-64 |
|
SHA-256 checksum How to use checksums |
d28c6e8d51be9483f6717c28241196eca67eaef9ef57d7975f28fa4fe9ead376
|
|
BLAKE2b-256 checksum How to use checksums |
e3b0fc2f4cfb6354bb375e43a08daf3fc41cf98f56c443ee5f4063f39fd510b2
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.10.12
|
Release files / css_inline-0.22.1-cp310-abi3-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl
| Download URL | css_inline-0.22.1-cp310-abi3-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl |
|---|---|
| Size | 3.6 MB |
| Tags | CPython 3.10 abi3 macOS 10.12+ universal2 (ARM64, x86-64) macOS 10.12+ x86-64 macOS 11.0+ ARM64 |
|
SHA-256 checksum How to use checksums |
422e4e5a567f1ba6b418963ed989620427aa27948c174dad21907be6d3294c46
|
|
BLAKE2b-256 checksum How to use checksums |
9a9156705dfaa0d347b926ebb6f86962554a09df5bdb2328e0ca5d48115b826a
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.10.12
|