Docs
Systhema Design (opens in new tab)
Unreleased

Production CSS optimization

Structural passes, variable obfuscation and prerender-only class obfuscation.

On this page

Systhema can post-process its CSS for production: three structural passes that simplify the token output, and two obfuscators that shorten variable and class names. Everything sits under one optimization key in systhema.config, everything is off by default, and every pass runs only when NODE_ENV is production, so development output stays readable.

Choosing what to turn onLink to this section

SettingWhat it doesConstraint
optimization: trueThe three structural passes. Never obfuscation.None.
obfuscateVariables: trueRenames Systhema's CSS custom properties.Variable names used in CMS content need an ignore entry.
obfuscateClasses: trueRenames Systhema's class names after the build.Fully prerendered sites only; needs the systhema-core obfuscate step.

Be clear about what each buys. The structural passes remove a good share of the raw CSS, but the bytes they remove are the repetitive ones that brotli already compresses to almost nothing, so the file a visitor downloads stays about the same size. They make the stylesheet cheaper to decompress and parse, not smaller to transfer. The transfer savings come from the obfuscators: variable obfuscation cuts roughly a tenth of the compressed front-end CSS on the default tokens, and class obfuscation a few percent more. Each obfuscator has a constraint, so turn them on deliberately.

Structural passesLink to this section

optimization: true turns on the three structural passes:

systhema.config.ts
import type { SysthemaConfig } from '@systhemaui/core'

const config: SysthemaConfig = {
  optimization: true,
}

export default config

The object form names each pass, so you can pick them one by one and add the obfuscators:

systhema.config.ts
import type { SysthemaConfig } from '@systhemaui/core'

const config: SysthemaConfig = {
  optimization: {
    pruneUnchangedBreakpoints: true,
    mergeThemeDuplicates: true,
    variableReferencesResolution: true,
    obfuscateVariables: true,
    obfuscateClasses: false,
  },
}

export default config
  • pruneUnchangedBreakpoints drops a breakpoint's variable declarations when they repeat the value of the breakpoint below.
  • mergeThemeDuplicates merges color modes that produce identical variable sets into one rule, for example a custom theme that inherits a mode unchanged.
  • variableReferencesResolution replaces a var(--x) with its value when --x is defined in exactly one place.

None of them changes how a page renders. The Systhema templates ship with the structural passes on.

ObfuscationLink to this section

Both obfuscators accept true for the defaults, or an options object of the type SysthemaObfuscationStyle (exported from @systhemaui/core):

systhema.config.ts
import type { SysthemaConfig } from '@systhemaui/core'

const config: SysthemaConfig = {
  optimization: {
    obfuscateVariables: { prefix: '--x', ignore: ['--color-foundations-'] },
    obfuscateClasses: { prefix: 'ui-', ignore: ['keep-*'] },
  },
}

export default config
OptionDefault (variables / classes)Meaning
prefix--s / sys-Prefix of every generated name.
suffix''Suffix of every generated name.
methodsequentialsequential (short names, best compression), random, hash, or none (prefix only).
length4Name length for random and hash.
ignore[]Names to keep: exact ('foo') or a family ('foo-' or 'foo*').
seed'systhema'Seed for random, so builds are reproducible.

Variable obfuscationLink to this section

obfuscateVariables renames every custom property Systhema defines, at the place it is defined and in every var() that references it, while the CSS is generated (--color-foundations-bg becomes something like --sa). It is a pure CSS transform, independent of your bundler, and safe on every project, Payload sites included. It leaves alone the --screen-* and --breakpoint-* variables that the vw utilities read at runtime, Tailwind's own variables, and anything in ignore.

Names written outside Systhema's stylesheet still say --color-foundations-bg after the build, and without help would resolve to nothing: a border disappears, a heading turns black. Two places do this:

  1. Your own CSS that references a Systhema variable by name.
  2. Inline styles stored in the CMS, such as a text state whose CSS is color: var(--color-foundations-decorative).

Run systhema-core obfuscate after next build (see Next.js setup) and it repairs what it can see: it rewrites every remaining var(--name) in the emitted CSS, HTML, RSC payloads and JavaScript, re-points your own overrides (:root { --color-foundations-line: … }) at the new names, and prints a warning listing every reference it could not resolve, with the ignore entry to add.

It cannot see content authored after the build. A page published tomorrow renders its inline style from the database with the original name.

Two habits follow from this:

  • Write readable token names everywhere, in project CSS and in inline styles. Don't replace a token with its literal value so it "survives" the build; that only breaks theming.
  • Don't check for a variable by name on an optimized build. Searching the production CSS for --color-foundations-bg, or reading it with getComputedStyle(el).getPropertyValue('--color-foundations-bg'), finds nothing because the name is gone. Check the computed value (getComputedStyle(el).backgroundColor), or check a development build.

Class obfuscationLink to this section

obfuscateClasses configures a post-build step, systhema-core obfuscate, which renames Systhema's front-end class names to short tokens (text-h1 becomes sys-a) consistently across the compiled CSS, the prerendered HTML, the RSC payloads and the client JavaScript. It renames only class names in Systhema's own set, and only where they appear as classes, so URLs, data attributes and other strings stay untouched. The flag on its own does nothing: without the post-build step there is no rename and no error.

So systhema-core obfuscate refuses to run the class pass when it finds anything that renders at runtime: a Payload project, ISR routes, on-demand dynamic routes, or a force-dynamic or revalidate export. It exits with a non-zero code and lists the reasons. A site you know is fully prerendered can override the check with --allow-dynamic. Variable obfuscation has no such restriction.

Next.js setupLink to this section

Add a separate build script; the regular build stays as it is:

package.json
{
  "scripts": {
    "build:obfuscated": "next build && cross-env NODE_ENV=production systhema-core obfuscate"
  }
}

systhema-core obfuscate [dir] works on .next by default and reads optimization from your systhema.config. It does nothing unless obfuscateClasses or obfuscateVariables is on and NODE_ENV is production, which cross-env guarantees here (add it with pnpm add -D cross-env). Wire the script whenever either obfuscator is on: with obfuscateVariables alone it still repairs variable references and reports orphans. Running it twice is harmless.

systhema doctor warns when obfuscateClasses is on without this script, and when it is on in a Payload project at all.

Classes that keep their namesLink to this section

Some classes must stay literal because something other than the stylesheet produces or looks them up, and Systhema keeps them automatically:

  • class families that components assemble at runtime (bg-layout-*, button-*, card-*, accordion-*, column-span-*, color-*, the posts listing and archive filter families, and the animation classes);
  • classes Systhema's listeners find by selector (menu-* and the header toggles, accordion*, parallax, archive-listing) and the state classes they toggle (is-open, is-active, is-scrolling-down, scroll-on-top);
  • classes written by bundled libraries: Swiper (swiper-slide, swiper-wrapper) and the cookie banner (cc--systhema and its parts).

Add your own to obfuscateClasses.ignore: any class you build from strings, query with querySelector, or toggle from a script.

Two more things to know:

  • The rename map comes from the stylesheets the prerendered homepage links. A route that loads its own stylesheet and styles a Systhema class there (.text-h1 span { … }) keeps the old selector while its markup is renamed, so that rule stops applying. Move such rules into the shared stylesheet, or add the classes to ignore.
  • In client JavaScript a string has no syntactic "class position", so a Systhema class that is also an HTML tag name (article, header, footer, menu) is protected by recognising JSX element types. A third-party script that passes such a bare tag name as data, as in document.createElement('footer'), could have it renamed.

The Payload Admin is never touched: its classes are not in the set.