Native CSS pruning
Understand how Master CSS prunes unused native CSS class rules from managed stylesheets.
Overview
Class scanning decides which classes are used. Native CSS pruning uses that usage graph to remove unused native CSS class rules from Master-managed stylesheets.
source usage + @import "@master/css" stylesheet-> used native class names-> pruned native CSSA stylesheet becomes Master-managed when it imports @master/css. Native class selector rules in that stylesheet are pruned by default.
@import "@master/css";.card { border: 1px solid rgb(0 0 0 / 12%);}.unused { color: red;}If source files use card but never use unused, the output keeps .card and removes .unused.
<article class="card">Card</article>.card { border: 1px solid rgb(0 0 0 / 12%);}Use Scanning latent classes for the source scanning model. This page focuses on what happens after a stylesheet enters the Master CSS pipeline.
Default pruning
Import @master/css from the CSS entry that should receive generated Master CSS. Ordinary class selector rules in that entry are pruned against detected source classes.
@import "@master/css";.app-shell { min-height: 100dvh;}.debug-outline { outline: 1px solid red;}The Master import is replaced by generated CSS. It is not emitted as a browser-facing package import by the integration.
.app-shell { min-height: 100dvh;}Use @preserve native; when the stylesheet contains class names that are intentionally invisible to the scanner.
@import "@master/css";@preserve native;.cms-card { color: red;}.injected-by-payment-widget { color: blue;}This stylesheet is still Master-managed, but its native CSS is preserved as written.
For exact stylesheet directive syntax, see CSS directives.
Import graph
When a Master-managed .css entry imports local relative .css files, those imports are expanded into the same pruning graph.
@import "@master/css";@import "./styles/cards.css";@import "./styles/buttons.css";.page { display: grid;}.card { border: 1px solid rgb(0 0 0 / 12%);}.unused-card { color: red;}.button { display: inline-flex;}.unused-button { color: blue;}export function App() { return ( <main className="page"> <article className="card"> <button className="button">Save</button> </article> </main> )}The compiled native CSS keeps .page, .card, and .button, then removes .unused-card and .unused-button.
src/style.css-> imports @master/css-> imports ./styles/cards.css-> imports ./styles/buttons.css-> pruned as one root graphOnly local relative .css imports are expanded this way. Bare package imports, remote imports, and non-CSS imports stay in the bundler's normal CSS pipeline unless they are one of the Master CSS entry imports.
Root scope
Pruning is root-scoped. The stylesheet directly processed by the integration is the root; imported files are dependencies of that root.
app/a/a.css -> imports ../shared.cssapp/b/b.css -> imports ../shared.css@import "@master/css";@import "../shared.css";.a-page { color: red;}@import "@master/css";@import "../shared.css";.b-page { color: blue;}.shared-card { border: 1px solid rgb(0 0 0 / 12%);}.only-used-in-b { color: blue;}If app/a/page.tsx uses a-page shared-card, then the a.css output keeps .a-page and .shared-card, but removes .b-page and .only-used-in-b.
If app/b/page.tsx uses b-page shared-card only-used-in-b, then the b.css output can keep .b-page, .shared-card, and .only-used-in-b.
The same imported file can therefore be pruned differently under different roots. If shared.css is also imported directly by JavaScript or by a framework route, then shared.css becomes its own root only when that direct import enters the Master CSS pipeline.
Scoped scanning
By default, a pruned stylesheet uses the global scanner scope. Add @source, @source not, @safelist, and @blocklist inside the stylesheet graph when that root needs a narrower or wider usage graph.
app/a/a.css -> scans app/a/page.tsx -> imports ../shared.cssapp/b/b.css -> scans app/b/page.tsx -> imports ../shared.css@import "@master/css";@source "./page.tsx";@import "../shared.css";.a-card { color: red;}.b-card { color: blue;}@import "@master/css";@source "./page.tsx";@import "../shared.css";.a-card { color: red;}.b-card { color: blue;}@safelist "shared-card";@blocklist "debug-*";.shared-card { border: 1px solid rgb(0 0 0 / 12%);}.debug-outline { outline: 1px solid red;}./page.tsx is resolved relative to the CSS file that declared it. The directives in shared.css are merged into whichever root imports it, so shared-card is included for both roots and debug-* is blocked for both roots.
export default function Page() { return <article className="a-card shared-card">A</article>}export default function Page() { return <article className="b-card shared-card">B</article>}The a.css output keeps .a-card and .shared-card. The b.css output keeps .b-card and .shared-card. Both roots remove .debug-outline because the imported @blocklist "debug-*"; directive belongs to each root scope.
If a root must scan a file, make sure no @source not pattern removes it.
@import "@master/css";@source "./**/*.tsx";@source not "./**/*.stories.tsx";Rule filtering
Native CSS pruning filters class selector rules by detected usage. A class rule is kept when its selector branch references a class that is used.
@import "@master/css";body { margin: 0;}.card,.unused:hover { border: 1px solid rgb(0 0 0 / 12%);}@media (width >= 48rem) { .card .title { font-weight: 700; } .debug-card { outline: 1px solid red; }}<article class="card"> <h2 class="title">Card</h2></article>The output keeps body, .card, and .card .title. It removes .unused:hover and .debug-card.
body { margin: 0;}.card { border: 1px solid rgb(0 0 0 / 12%);}@media (width >= 48rem) { .card .title { font-weight: 700; }}Rules without class selectors, such as body, html, :root, and keyframes, are not class-usage rules and are preserved. Empty at-rule blocks are removed after their unused class rules are pruned.
Manifest references
Pruned native CSS can reference variables and keyframes from the project CSS entry graph. When a kept native rule uses var(--*) or a manifest animation name, Master CSS includes the required manifest output.
@theme { --color-primary: #175cff;}@keyframes fade-in { from { opacity: 0; } to { opacity: 1; }}@import "@master/css";.notice { color: var(--color-primary); animation-name: fade-in;}.unused-notice { color: var(--color-primary);}<p class="notice">Saved</p>The kept .notice rule pulls in --color-primary and @keyframes fade-in. .unused-notice is removed, so it does not keep anything alive by itself.
Included classes
If a native class is used by a bounded external source, include it explicitly so pruning keeps the rule.
@import "@master/css";@safelist "dialog-open dialog-closing";.dialog-open { overflow: hidden;}.dialog-closing { pointer-events: none;}Use this for known class contracts. Do not use it as a replacement for putting normal application classes in source.
Checklist
- Import
@master/cssfrom CSS entries that should receive generated Master CSS and native CSS pruning. - Add
@preserve native;to Master-managed entries whose native CSS must be preserved. - Remember that local relative
.cssimports follow the root's pruning decision. - Use
@safelistfor native classes used by bounded external systems. - Keep vendor, CMS, or third-party styles outside Master-managed roots unless their class usage is visible to the scanner or protected with
@preserve native;.