Skip to content
NuvoUI

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.

Terminal
npm install -D sass @nuvoui/core @fullhuman/postcss-purgecss

Set up

  1. Write the configuration once

    A partial that forwards @nuvoui/core/mixins with 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);
  2. 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";
  3. Purge the global stylesheet in production builds

    NuvoUI class names contain @ (p-4@md), : (hover:bg-brand, group-hover:bg-blue) and . (p-0.5 with a decimal spacing key), so the extractor keeps those characters inside a token. dynamicAttributes keeps every data-theme and data-contrast block, because the theme is often set from a script. :target matches no element in the markup, so it is safelisted. The wrapper runs only in vite build and only on global.scss: CSS modules have hashed class names that never appear in the source, and they hold only your own rules.

    postcss.config.js
    import 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.

src/App.tsx
// 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>
postcss.config.js
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.

Terminal
npm run build

What 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.