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:Falseresolve_css_variables. Replacevar()references with the values of the custom properties they refer to and drop the--*declarations. 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.23.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | |
|---|---|---|---|---|
| css_inline-0.23.0.tar.gz | 104.2 kB | Without provenance | Details |
Built distributions (wheels)
Total release size: 29.8 MB
Release files / css_inline-0.23.0.tar.gz
| Download URL | css_inline-0.23.0.tar.gz |
|---|---|
| Size | 104.2 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
400878761fd37699a0bd06d1b842b3941b0346baa051d70d29dd418a541d4a91
|
|
BLAKE2b-256 checksum How to use checksums |
3bc4535a38ccee6b847e64b903dd2d787f7ef06e71aa37a599fe1d736a617bf5
|
| 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.23.0-pp311-pypy311_pp73-manylinux_2_24_x86_64.whl
| Download URL | css_inline-0.23.0-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 |
fd37cd39fe1445edc412188c94624921c94948272e842f677f5ff316371dcf6c
|
|
BLAKE2b-256 checksum How to use checksums |
d4d027868c95caca7dbcdc77b4e2d041da865622abfb7a4d8d9ca545bab4390f
|
| 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.23.0-pp311-pypy311_pp73-manylinux_2_24_aarch64.whl
| Download URL | css_inline-0.23.0-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 |
e583a6e2e0defa9f9bb8ce413f830722f3e13ad1e215133bfbe8faeccb6a6d84
|
|
BLAKE2b-256 checksum How to use checksums |
e90d2d1d55c2f84d9a1a3fc7493260794f0e9974dcba6d1f70d9d2cc98808887
|
| 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.23.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl
| Download URL | css_inline-0.23.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl |
|---|---|
| Size | 1.9 MB |
| Tags | PyPy 3.11 PyPy 3.11 7.3 macOS 10.12+ x86-64 |
|
SHA-256 checksum How to use checksums |
93274f2f9002759bb10374f28c7c6031faed0fb517306b104b5d51613553069a
|
|
BLAKE2b-256 checksum How to use checksums |
f972ab43713e951e16ce9e4c39e53cdb7ceb08b1ef5a6d7f0be244185ec3d2e3
|
| 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.23.0-cp310-abi3-win_amd64.whl
| Download URL | css_inline-0.23.0-cp310-abi3-win_amd64.whl |
|---|---|
| Size | 1.9 MB |
| Tags | CPython 3.10 Windows x86-64 abi3 |
|
SHA-256 checksum How to use checksums |
4de8645097f65ec66bb3f77c7a8534c114b83018bf776b69db4b07ba995fc735
|
|
BLAKE2b-256 checksum How to use checksums |
922773dc7111a523140999b2a822cca3c516030c8ddc979128c0677a59049622
|
| 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.23.0-cp310-abi3-win32.whl
| Download URL | css_inline-0.23.0-cp310-abi3-win32.whl |
|---|---|
| Size | 1.6 MB |
| Tags | CPython 3.10 Windows x86-32 abi3 |
|
SHA-256 checksum How to use checksums |
de84658351a49946a21e161531740723e662ece3646dd61ddb3a0043f3f9472b
|
|
BLAKE2b-256 checksum How to use checksums |
315ba9ca26a0b03be85b9ff8012865a4904c0e781e0291dc029ccf0bae2e6e24
|
| 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.23.0-cp310-abi3-pyemscripten_2026_0_wasm32.whl
| Download URL | css_inline-0.23.0-cp310-abi3-pyemscripten_2026_0_wasm32.whl |
|---|---|
| Size | 554.2 kB |
| Tags | CPython 3.10 PyEmscripten 2026.0+ WebAssembly abi3 |
|
SHA-256 checksum How to use checksums |
5c1ce00715c4fa2881211125a3ad5a3598d1e78fdf8f6cfa6dd50eeb3c13959e
|
|
BLAKE2b-256 checksum How to use checksums |
12f68ecd7ec1e10478f9d36d371c92b4fe7dc65005652d9d63bbeacd23008546
|
| 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.23.0-cp310-abi3-pyemscripten_2025_0_wasm32.whl
| Download URL | css_inline-0.23.0-cp310-abi3-pyemscripten_2025_0_wasm32.whl |
|---|---|
| Size | 560.8 kB |
| Tags | CPython 3.10 PyEmscripten 2025.0+ WebAssembly abi3 |
|
SHA-256 checksum How to use checksums |
c456bd27a46aeb37c1eed437371136459e93512aee0c86b3ead4f1213cd761ed
|
|
BLAKE2b-256 checksum How to use checksums |
208522213bed546e26cec592aad05ab52b9872f0b1573c8fb7c20527f890c620
|
| 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.23.0-cp310-abi3-musllinux_1_2_x86_64.whl
| Download URL | css_inline-0.23.0-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 |
37746c1f68dcc25f9a25927a077fae4fa2454460926678495c5ad5d712ffbb22
|
|
BLAKE2b-256 checksum How to use checksums |
d7a9582f9aa84459605b4c89849495ca6fc67b662973c296943354190325f868
|
| 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.23.0-cp310-abi3-musllinux_1_2_armv7l.whl
| Download URL | css_inline-0.23.0-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 |
3c87d9dcb018dfef40b715eece6a168e0e80fc09f8b701b1367ac7849995e6e3
|
|
BLAKE2b-256 checksum How to use checksums |
89ca5186ee81836887d9a20b40bc7af561e7eddf5794dd35e1a48fd72ca7387b
|
| 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.23.0-cp310-abi3-musllinux_1_2_aarch64.whl
| Download URL | css_inline-0.23.0-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 |
6cd30a584fd83f3eda6fae2dd54e0b4f8b5c20f2227221ff1f9cfe2a5a37d3fa
|
|
BLAKE2b-256 checksum How to use checksums |
f75196629fef42149bd65a3ee6dee67a4b2f545bb898050c5645a09423b0f882
|
| 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.23.0-cp310-abi3-manylinux_2_24_armv7l.whl
| Download URL | css_inline-0.23.0-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 |
47d46e81ed994e22e282a439d64ba978d2c7228354f61a20ca92fe628a999991
|
|
BLAKE2b-256 checksum How to use checksums |
571067b12b85ff43e5df01dcaa7602750cb0124280f8d2dcd9705a3f57a82875
|
| 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.23.0-cp310-abi3-manylinux_2_24_aarch64.whl
| Download URL | css_inline-0.23.0-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 |
fc7afed9a1e1ec6810484b023a6e48af0f39fc6a13a423cb3e60fd0f6c63fe38
|
|
BLAKE2b-256 checksum How to use checksums |
65b57205b8aa901581fba96de750cdc8eb4721f1f53600e6163256b242e00718
|
| 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.23.0-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
| Download URL | css_inline-0.23.0-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 |
f49665c7f506f527340b4f1456e13daf89b620e5cc9336986645286ed7a7a9e1
|
|
BLAKE2b-256 checksum How to use checksums |
408b6dbe85bad380260c1fc75daf75ae692f14f4113b22820b146433e052b19f
|
| 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.23.0-cp310-abi3-manylinux_2_12_i686.manylinux2010_i686.whl
| Download URL | css_inline-0.23.0-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 |
dd0cbcd61ab57bf0b56f8675b790a9c8f3b970668ea89ca79a9b0ccaa35d9aff
|
|
BLAKE2b-256 checksum How to use checksums |
9401f27933d15926fd518ed630da2e93c36a795c4743f7f57dc21d8d8571c65e
|
| 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.23.0-cp310-abi3-macosx_10_12_x86_64.whl
| Download URL | css_inline-0.23.0-cp310-abi3-macosx_10_12_x86_64.whl |
|---|---|
| Size | 1.9 MB |
| Tags | CPython 3.10 abi3 macOS 10.12+ x86-64 |
|
SHA-256 checksum How to use checksums |
297fff9619831475f306ec07e49bb98c4ceeacae0be1032677be1a425c3a7eef
|
|
BLAKE2b-256 checksum How to use checksums |
6bc3f134a7b5d4ff9ef2f0754a6d2d8f023a164f6a5993d0033bf816aa8f4807
|
| 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.23.0-cp310-abi3-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl
| Download URL | css_inline-0.23.0-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 |
a5f0be6f749106e4e952de0fa0f1a7c7c0716a1e6354e9e4a98dd5330ca0764b
|
|
BLAKE2b-256 checksum How to use checksums |
6bb9165dde90e0ae9d27e65666de62f3c8caa607bdbc0e8dd0b26e87943ebff7
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.10.12
|
Security for css-inline 0.23.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.