# Automattic/juice: Inlining CSS for HTML Email

> Juice takes an HTML document and moves the rules from its stylesheets into style attributes, so clients that strip head CSS still render the design. It is a build-time tool for email and for HTML embedded in third-party pages.

**Automattic/juice** — Juice inlines CSS stylesheets into your HTML source.

- Repository: https://github.com/Automattic/juice
- Stars: 3,282 · Forks: 233
- Language: JavaScript
- License: MIT
- Published: 2026-09-24 · Updated: 2026-09-24 · Language: en
- Canonical page: https://hysenlabs.com/projects/automattic-juice

## The email client problem Juice was built for

Most email clients ignore or strip the <style> element in the head of a message. Gmail has historically removed it, and Outlook's rendering engine has its own limits. A stylesheet that works in a browser therefore produces an unstyled message. The workaround the industry settled on is to put the declarations directly on each element as a style attribute, which every client honours. Doing that by hand does not scale past a handful of rules.

Juice automates that rewrite. Given HTML, it reads the CSS from <style> tags and from linked stylesheets, computes which declarations apply to which elements, and writes them into the style attribute. The README states the purpose in one line: "Given HTML, Juice will inline your CSS properties into the style attribute." The audience is anyone generating email markup programmatically, plus anyone embedding HTML into a third-party site that sanitises away stylesheet tags.

## Inside the inlining pass: cheerio, PostCSS and specificity

The package.json lists cheerio 1.2.0, postcss, postcss-nesting, postcss-safe-parser and postcss-selector-parser as dependencies, and that set explains the data flow. Cheerio parses the HTML into a DOM-like tree. PostCSS parses the CSS. The selector parser works out which rules match which nodes, and the result is written back onto the nodes as inline declarations before the tree is serialised to a string.

Specificity decides the winner. When two rules set the same property on the same element, only the declaration with the higher specificity is inlined by default. The inlineDuplicateProperties option changes that: set it to true and every declaration for an identical property is written out, which the README frames as useful for progressive enhancement, where you want a fallback value followed by a modern one.

The modern CSS support is broader than older inliners. Nested rules following CSS Nesting Module Level 1, such as .card { &:hover { ... } }, are flattened before inlining. The @container and @layer at-rules pass through verbatim rather than being resolved. Specificity for :is(), :where(), :has() and :not() is computed per the CSS Selectors Level 4 spec, which matters because :where() contributes zero specificity and a naive implementation would get that wrong.

## Installing Juice and inlining your first document

Juice is published on npm as juice and requires Node 22.12.0 or newer, per the engines field in package.json. The package is ESM: the README's examples use import syntax.

```bash
npm install juice
```

A first call takes an HTML string and returns a new string. Remote resources are not fetched by this function.

```js
import juice from 'juice';

const result = juice("<style>div{color:red;}</style><div/>");
```

The README gives the output for that exact input as a div carrying style="color: red;". If you see the style attribute populated and the original <style> tag gone, the pipeline worked; removeStyleTags defaults to true.

When the CSS lives in a linked stylesheet or the HTML references remote images, use the resource-aware entry points instead. Both take a callback.

```js
juice.juiceResources(html, options, callback)
juice.juiceFile(filePath, options, callback)
```

Remote fetching is delegated to web-resource-inliner 8.0.0, and the webResources option is passed straight through to it, so the README points there for the fetch configuration. If you already have a cheerio tree, juice.juiceDocument($, options) mutates it in place and returns the same instance. The README warns to use the same cheerio version that Juice uses. There is also a CLI, registered as bin/juice in package.json, which the README describes as exposing a smaller set of options than the module.

## What gets preserved, and why the defaults surprise people

Not every CSS construct can be expressed as an inline declaration. Media queries, @font-face blocks, keyframes and pseudo-selectors have no equivalent in a style attribute, so Juice keeps them in a <style> tag instead of discarding them. The options preserveMediaQueries, preserveFontFaces, preserveKeyFrames and preservePseudos all default to true, and the README describes them as refinements that apply when removeStyleTags is true. Only the preserved content survives; other rules are removed.

insertPreservedExtraCss defaults to true and controls where that leftover <style> element goes. The README gives an order of preference: into head, then body, then at the end of the document. It can also be a string treated as a CSS, jQuery or cheerio selector, in which case the tag is appended to the end of the first match.

inlinePseudoElements is the option most likely to cause trouble, and it defaults to false for good reason. Setting it to true inserts ::before and ::after content as real <span> elements. The README states plainly that this modifies the DOM and may conflict with CSS selectors elsewhere on the page, naming :last-child as an example. A generated span changes which element is last, so a rule keyed to :last-child can now match the wrong node. Turn this on only when you have checked the selectors in your own template.

Two smaller defaults are worth knowing. resolveCSSVariables is true, so var() references are resolved before inlining. preserveImportant is false, so !important is dropped from the inlined values unless you ask for it.

## Where Juice is the wrong tool

Juice does not make an email render correctly. It removes one class of failure, the stripped stylesheet, and leaves the rest. Client-specific quirks, table layout constraints and the CSS properties each client supports are outside its scope; the README points readers to Can I Email for that. If your message looks wrong after inlining, the inliner is rarely the cause.

The xmlMode option is a sharp edge. It outputs XML or XHTML with all tags closed, and the README warns that the input must also be valid XML or XHTML, otherwise you get undesirable results. Feeding it a typical HTML fragment with unclosed tags is a mistake, not a graceful degradation.

Juice is also not a sanitiser. Inlining CSS into a document does not remove scripts or untrusted markup, and nothing in the README presents it as a security boundary. If you are embedding third-party HTML, the inlining step and the sanitising step are separate problems.

Finally, the browser entry point is not a substitute for a build step. The package exposes client.js under the browser condition in its exports map, but the README's own recommendation for trying it out is a hosted web client at automattic.github.io/juice. Treat the browser build as a convenience, not the primary integration path.

## Juice compared with Premailer and MJML

The closest alternative in the Node ecosystem is Premailer, which inlines CSS through a headless browser rather than a parser. That difference shows up in what each can resolve. A browser evaluates the cascade as a browser would, which is useful when your CSS depends on computed layout, but it means a heavier runtime and a dependency on a browser binary. Juice stays in JavaScript: cheerio for the tree, PostCSS for the stylesheet, no browser process. The trade-off is that Juice reasons about the CSS statically, which is why the README documents explicit handling for nesting, :is(), :where(), :has() and :not() rather than relying on a rendering engine.

MJML sits at a different layer. It is a markup language that compiles to email HTML, and it can inline as part of that compilation. If you are authoring email from scratch, starting from MJML's components means you never write the table markup or the CSS that Juice would later move. If you already have HTML and CSS, whether hand-written or produced by another template system, Juice operates on that existing output, which is a smaller change to adopt. The two are not mutually exclusive: MJML's output is HTML, and an inlining step can run on it.

## Maintenance, licence and upgrade cost

The repository is not archived, and the last push was on 2026-09-21. Releases are frequent: v12.1.3 on 2026-09-09, v12.1.2 on 2026-08-04 and v12.1.1 on 2026-06-15. The package is published under the MIT licence, which permits commercial use and modification; the LICENSE.md file is the authoritative text and this article is not legal advice.

The upgrade cost is mostly the Node floor. The engines field requires Node 22.12.0 or newer, so a project pinned to an older runtime cannot install the current version without a runtime upgrade. The dependency set is small and current: cheerio 1.2.0, commander 14, postcss 8.5, postcss-nesting 14 and web-resource-inliner 8. The cheerio version is the one to watch. Because juice.juiceDocument and juice.inlineDocument accept a cheerio instance, a mismatch between your cheerio and Juice's is a real failure mode, and the README calls it out directly. If you use those methods, keep the dependency aligned rather than relying on npm to deduplicate.

The test setup is visible in package.json: vitest with coverage, plus a separate TypeScript check that compiles test/typescript and removes the emitted file. Type definitions ship as index.d.ts, so TypeScript consumers get types without a @types package.

## FAQ

The questions below cover the points readers raise most often about Juice.

## Conclusion

Adopt Juice when you generate HTML email or embed markup in a page whose host strips head CSS, and when a Node step in your build is acceptable. Do not adopt it if you need a browser-side inliner, if you cannot run Node 22.12.0 or newer, or if you expected it to be a security scanner. Before committing, verify how your template renders with removeStyleTags, preserveMediaQueries and inlinePseudoElements at their defaults, because those three decide what survives into the sent message.

## FAQ

### How do I install Juice?

Install it from npm with npm install juice. The package requires Node 22.12.0 or newer, according to the engines field in package.json, and it is published as an ESM module.

### What does Juice actually do to my HTML?

It reads the CSS from style tags and linked stylesheets, works out which declarations apply to each element, and writes them into the element's style attribute. The README's example turns a div with a style rule into a div carrying style="color: red;", with the original style tag removed by default.

### Does Juice fetch remote stylesheets and images?

Only through juiceResources and juiceFile, which the README says fetch remote resources. The plain juice() function does not fetch remote resources. Remote fetching is delegated to web-resource-inliner, and the webResources option is passed through to it.

### Why are my media queries still in a style tag after inlining?

Media queries, font faces, keyframes and pseudo-selectors cannot be expressed as inline declarations, so Juice preserves them in a style tag. preserveMediaQueries, preserveFontFaces, preserveKeyFrames and preservePseudos all default to true, and insertPreservedExtraCss controls where that tag is appended.

### What Node version does Juice need?

Node 22.12.0 or newer, per the engines field in package.json. A project on an older runtime cannot install the current release without upgrading Node.

## Sources

- [Automattic/juice on GitHub](https://github.com/Automattic/juice)
- [Issues](https://github.com/Automattic/juice/issues)
- [License: MIT](https://github.com/Automattic/juice/blob/master/LICENSE)
- [README](https://github.com/Automattic/juice/blob/master/README.md)
- [Releases](https://github.com/Automattic/juice/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/automattic-juice
