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 is preserved by default. Explicit native CSS pruning uses that usage graph to remove unused class rules from selected stylesheets.
source usage + stylesheet with @prune native; -> used native class names -> pruned native CSSImporting @master/css supplies the preset and generated CSS entry. Add @prune native; to opt this source file into pruning; the directive does not affect its imports. Passing class names to a compiler API alone never enables pruning.
@prune native;@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.
Explicit pruning
Import @master/css from the CSS entry that should receive generated Master CSS. Native class selector rules remain intact unless this file declares @prune native; or the integration explicitly enables pruneNativeCSS: true. The integration option applies only to project-owned sources.
@prune native;@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
Resolved CSS imports retain their own source boundaries. Each file opts in with @prune native;; an importing file cannot enable pruning in its dependencies. With the integration-wide pruneNativeCSS: true option, project-owned dependencies also participate unless they declare @preserve native;. Qualified native imports keep their media, supports and layer wrappers.
@prune native;@import "@master/css";@import "./styles/cards.css";@import "./styles/buttons.css";.page { display: grid;}@prune native;.card { border: 1px solid rgb(0 0 0 / 12%);}.unused-card { color: red;}@prune native;.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 -> each opted-in source pruned with root usagePackage and remote styles do not become project-owned merely because the project imports them. Preserve files with externally controlled classes, or opt in only where the complete class vocabulary is available.
Root scope
Pruning activation belongs to each source file. Once enabled, filtering uses the importing root’s usage scope. The same opted-in shared source can therefore retain different classes under different roots.
app/a/a.css -> imports ../shared.cssapp/b/b.css -> imports ../shared.css@prune native;@import "@master/css";@import "../shared.css";.a-page { color: red;}@prune native;@import "@master/css";@import "../shared.css";.b-page { color: blue;}@prune native;.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@prune native;@import "@master/css";@source "./page.tsx";@import "../shared.css";.a-card { color: red;}.b-card { color: blue;}@prune native;@import "@master/css";@source "./page.tsx";@import "../shared.css";.a-card { color: red;}.b-card { color: blue;}@prune native;@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.
@prune native;@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; }}@prune native;@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.
@prune native;@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. - Enable pruning separately with
@prune native;orpruneNativeCSS: true. - Add
@preserve native;to Master-managed entries whose native CSS must be preserved. - Check every source boundary:
@prune native;does not propagate to imports. - Use
@safelistfor native classes used by bounded external systems. - Use
@preserve native;for files with external class vocabularies when project-wide pruning is enabled.