Build & Delivery

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 CSS

A 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 graph

Only 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/css from 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 .css imports follow the root's pruning decision.
  • Use @safelist for 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;.

  • Master UI


© 2026 Aoyue Design LLC.MIT License
Trademark Policy