Setup
Ship only the classes you use
Keep every utility class while you develop, and ship only the classes your markup uses. Tested with React and Vite: the global stylesheet went from 990 KB to 37 KB minified, 92 KB to 5.6 KB gzipped, with identical computed styles.
Requires @nuvoui/core 2.1.0 or later.
Install
Start from the React with Vite setup. @fullhuman/postcss-purgecss 8.0.0 is a PostCSS plugin, and Vite loads it from postcss.config.js with no change to vite.config.ts.
npm install -D sass @nuvoui/core @fullhuman/postcss-purgecssSet up
Write the configuration once
A partial that forwards
@nuvoui/core/mixinswith your settings. The mixins entry has the same API as@nuvoui/core(apply(), every mixin,token(),with()) and outputs no CSS.src/styles/_nuvo.scss@forward "@nuvoui/core/mixins" with ($brand: #4361ee, $generate-utility-classes: true);Create the global stylesheet
It emits the tokens, the reset, the base styles and the utility classes with your configuration.
@use "nuvo"comes first, so core loads with the configured brand. Keep the utility classes in this one file, so the purge can target it alone.src/styles/global.scss@use "nuvo"; @use "@nuvoui/core";Purge the global stylesheet in production builds
NuvoUI class names contain
@(p-4@md),:(hover:bg-brand,group-hover:bg-blue) and.(p-0.5with a decimal spacing key), so the extractor keeps those characters inside a token.dynamicAttributeskeeps everydata-themeanddata-contrastblock, because the theme is often set from a script.:targetmatches no element in the markup, so it is safelisted. The wrapper runs only invite buildand only onglobal.scss: CSS modules have hashed class names that never appear in the source, and they hold only your own rules.postcss.config.jsimport purgeCSSPlugin from '@fullhuman/postcss-purgecss' const purge = purgeCSSPlugin({ content: ['./index.html', './src/**/*.{js,jsx,ts,tsx}'], defaultExtractor: (content) => content.match(/[\w@:.-]+(?<![.:])/g) ?? [], dynamicAttributes: ['data-theme', 'data-contrast'], safelist: [':target', /^text-(sm|md|lg)$/], }) export default { plugins: [ { postcssPlugin: 'purge-global-stylesheet', OnceExit(root, helpers) { if (process.env.NODE_ENV !== 'production') return if (!/[\\/]global\.scss$/.test(root.source?.input.file ?? '')) return return purge.OnceExit(root, helpers) }, }, ], }
Classes built from strings
PurgeCSS reads the source files as text. A complete class name anywhere in a file is kept, in a ternary or an object of variants alike. A name assembled from parts, such as text-${size}, is never written out, so it is removed. Write complete names, or add them to safelist as strings or regular expressions.
// Kept: complete names
const tone = { paid: 'bg-success-subtle text-success', due: 'bg-warning-subtle text-warning' }
// Removed unless safelisted: text-lg is never written out
<p className={`text-muted text-${size}`}>Dark section</p>safelist: [':target', /^text-(sm|md|lg)$/],How it was tested
A test page used responsive classes (p-8@lg, hide@md, cols-3@md), state classes (hover:bg-brand-subtle, focus:border-brand, hover:opacity-75@sm), group-hover:bg-blue, compound classes (flex col row@md, .flex > .grow, flex stretch, fill-6), decimal keys (p-0.5@xl, mt-0.5, with a 0.5 spacing key), tooltips, data-theme="dark" and "inverse" sections, animate-spin and a CSS module. All 113 rules that apply to the page were kept with the same declarations. The computed styles of every element, pseudo-elements included, were identical before and after the purge at 375, 800 and 1440 pixels wide, with no theme, data-theme="dark" and "light", in light and dark system modes, on hover and on focus. The keyframes, the @property registrations, the @layer order and every :root and [data-theme] block are kept.
npm run buildWhat gets emitted
In the sample (@nuvoui/core 2.1.0, every utility class on, Vite production build) the stylesheet is 990 KB minified, 92 KB gzipped, without the purge, and 37 KB minified, 5.6 KB gzipped, with it.
The purged file is smaller than the 42 KB base of a build without classes, because reset and base rules for elements the page never uses, and tooltip positions it never sets, are removed too. Add an element or a class to the source and the next build keeps its rules. Development builds skip the purge, so every class is available while you work.