Compiler Engine
FrontAlign ships with a built-in JIT Compiler Engine. It analyzes your project files, extracts active CSS utilities, processes your frontalign.config.js configuration, and outputs an optimized frontalign.build.css file containing only what your project actually needs.
Beyond utility extraction, the compiler also:
- Generates theme tokens from
frontalign.config.js - Compiles dark mode variables
- Compiles responsive breakpoints into media queries
- Creates font utility classes and custom classes
- Purges unused @keyframes blocks
- Removes unused CSS variables while preserving their dependency chains
The result is a lightweight, production-ready stylesheet that contains only the styles, variables, and generated assets your project actually uses.
FrontAlign does not simply scan class names and copy matching CSS rules. It builds a dependency-aware CSS output.
The compiler reads FrontAlign's core CSS, extends :root with your theme (including extend, dark, dark.extend, and breakpoints), generates fonts and classes, scans your source files, resolves used classes and variables, and finally writes everything out organized into cascade layers — theme, base, components, utilities, custom — in that order.
Production Build
npx frontalign build
Build mode compiles the final CSS and applies production optimization. The output is written to: public/frontalign.build.css
Import it in your application:
import '../public/frontalign.build.css'
Or include it in HTML:
<link rel="stylesheet" href="/frontalign.build.css" />
Development Watch Mode
npx frontalign dev
Watch mode monitors your config file, FrontAlign's main CSS file, and your configured scan targets. When any file changes, frontalign.build.css is rebuilt automatically.
When frontalign.css itself changes, the compiler cache is automatically invalidated and a full rebuild is triggered.
frontalign.config.js Setup
The compiler requires a frontalign.config.js file in your project root. Here is a complete example showing all available options:
export default {
jit: {
scan: ['./app', './components', './pages'],
safelist: ['button', 'is-primary', 'is-active'],
extensions: ['mdx'],
debug: false,
concurrency: 50
},
theme: {
primary: '#2563eb',
body: '#ffffff',
bodyText: '#111827',
font: 'Inter, sans-serif',
extend: {
'brand-surface': '#f8fafc',
'brand-border': '#e2e8f0'
},
dark: {
body: '#0f172a',
bodyText: '#e5e7eb',
extend: {
'brand-surface': '#111827',
'brand-border': '#334155'
}
},
breakpoints: {
sm: '640px',
md: '864px',
lg: '1120px',
xl: '1408px',
'2xl': '1792px'
}
},
fonts: [
{
family: 'Inter',
weights: '400;500;600;700',
alias: 'inter',
category: 'sans-serif'
}
],
classes: {
'hero-card': {
'background': 'var(--brand-surface)',
'border': '1px solid var(--brand-border)',
'border-radius': '1rem',
'padding': '2rem',
dark: {
'background': 'var(--brand-surface)',
'border-color': 'var(--brand-border)'
}
},
'text-gradient': 'background: linear-gradient(90deg, #2563eb, #60a5fa); -webkit-background-clip: text; color: transparent;'
}
}
dark.extendworks the same way as the root-leveltheme.extend: it lets you define dark-mode-only values for custom tokens. Any key placed directly underdarkthat isn't one of FrontAlign's recognized theme keys (body,bodyText,primary,primaryContrast,font,fontMono,spaceXs–space2xl) is silently ignored unless it's nested underdark.extend.
CSS Output
The config above compiles into the following frontalign.build.css output. FrontAlign organizes everything into five CSS cascade layers, always in this order — theme, base, components, utilities, custom — so specificity never has to be fought with source order:
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap');
@layer theme, base, components, utilities, custom;
@layer theme {
:root {
--body: #ffffff;
--body-text: #111827;
--primary: #2563eb;
--font: Inter, sans-serif;
--brand-surface: #f8fafc;
--brand-border: #e2e8f0;
}
[fa-theme="dark"] {
--body: #0f172a;
--body-text: #e5e7eb;
--brand-surface: #111827;
--brand-border: #334155;
}
}
@layer base {
/* FrontAlign's base/reset styles */
}
@layer components {
/* FrontAlign's own bundled component styles, filtered by the JIT scan */
}
@layer utilities {
@media (min-width: 640px) {
/* sm utilities */
}
@media (min-width: 864px) {
/* md utilities */
}
/* ... active utility classes extracted by the JIT scan ... */
}
@layer custom {
.font-inter {
font-family: 'Inter', sans-serif;
}
.hero-card {
background: var(--brand-surface);
border: 1px solid var(--brand-border);
border-radius: 1rem;
padding: 2rem;
}
[fa-theme="dark"] .hero-card {
background: var(--brand-surface);
border-color: var(--brand-border);
}
.text-gradient {
background: linear-gradient(90deg, #2563eb, #60a5fa);
-webkit-background-clip: text;
color: transparent;
}
}
| Layer | Comes from |
|---|---|
theme | theme — root tokens, extend, dark (including dark.extend), and breakpoints |
base | FrontAlign's own base/reset styles |
components | FrontAlign's own bundled component styles, filtered by the JIT scan — not your classes config |
utilities | Breakpoint media queries and JIT-extracted utility classes from FrontAlign's bundled CSS |
custom | fonts-generated .font-* classes, plus every classes entry — object form (e.g. hero-card) and raw-string form (e.g. text-gradient) alike |
Because each layer is declared up front with @layer theme, base, components, utilities, custom;, a rule in custom always beats a rule of equal specificity in utilities, and so on — regardless of where either rule physically sits in the file.
All entries under
classes— whether written as a declaration object or a raw CSS string, with or without adarksub-key — compile into thecustomlayer. Thecomponentslayer is reserved for FrontAlign's own bundled component styles; it does not contain anything from your config.
Scan Targets
The jit.scan option tells FrontAlign where to look for used classes.
export default {
jit: {
scan: ['./app', './components']
}
}
When a folder path is provided, FrontAlign automatically scans all supported source files inside that folder. You can also pass direct file paths or glob patterns:
export default {
jit: {
scan: ['./app', './components/Button.tsx', './pages/**/*.jsx']
}
}
Supported Source Files
By default, FrontAlign only scans .js, .jsx, .ts, and .tsx files. Any other extension — including .html and .php — must be explicitly added via jit.extensions (see Scan Performance & Extensions below) before FrontAlign will scan files with that extension.
| Extension | Use case | Scanned by default? |
|---|---|---|
.js | JavaScript files | Yes |
.jsx | React JSX files | Yes |
.ts | TypeScript files | Yes |
.tsx | React TypeScript files | Yes |
.html | Static HTML files | No — requires jit.extensions: ['html'] |
.mdx | MDX documentation and content files | No — requires jit.extensions: ['mdx'] |
Class Extraction
FrontAlign detects classes from plain HTML, JSX, template strings, and common class composition helpers:
| Pattern | Example |
|---|---|
class | <div class="card p-4"> |
className | <div className="card p-4" /> |
className={ | |
| clsx('button', active && 'is-active') |
cva() | |
| |
Dynamic expressions inside template strings (${...}) are stripped during extraction, but the static class names surrounding them are collected.
Safelist
Use safelist when a class is generated dynamically and cannot be reliably detected from source code.
export default {
jit: {
scan: ['./app', './components'],
safelist: [
'button',
'is-primary',
'is-danger',
'is-active'
]
}
}
Safelisted classes are always included in the generated CSS output regardless of whether they appear in scanned files.
Scan Performance & Extensions
Beyond scan and safelist, jit accepts a few more options that control how scanning behaves:
| Option | Default | Description |
|---|---|---|
extensions | [] | Adds extra file extensions on top of the default js, jsx, ts, tsx. Required to scan .html, .mdx, or any other file type. |
concurrency | 50 | Maximum number of files read, hashed, and scanned in parallel. Lower this if you hit file-descriptor limits on very large projects. |
debug | false | When true, logs a warning to the console for any file the scanner could not resolve, stat, hash, or read, instead of silently skipping it. |
export default {
jit: {
scan: ['./app', './components', './content'],
extensions: ['html', 'mdx'],
concurrency: 100,
debug: true
}
}
Theme Tokens
Define your core design tokens in frontalign.config.js:
export default {
theme: {
body: '#ffffff',
bodyText: '#111827',
primary: '#2563eb',
font: 'Inter, sans-serif',
fontMono: 'Fira Code, monospace',
spaceXs: '0.25rem',
spaceSm: '0.5rem',
spaceMd: '1rem',
spaceLg: '1.5rem',
spaceXl: '2rem',
space2xl: '3rem'
}
}
These values are compiled into CSS variables:
| Config key | Generated CSS variable |
|---|---|
body | --body |
bodyText | --body-text |
primary | --primary |
font | --font |
fontMono | --font-monospace |
spaceXs | --space-xs |
spaceSm | --space-sm |
spaceMd | --space-md |
spaceLg | --space-lg |
spaceXl | --space-xl |
space2xl | --space-2xl |
Only the keys listed above are recognized. Any other key placed directly under
themeis ignored — usetheme.extendfor anything custom.
Extending Tokens
Use theme.extend to add custom CSS variables:
export default {
theme: {
extend: {
'brand-surface': '#f8fafc',
'brand-border': '#e2e8f0',
'--brand-radius': '1rem'
}
}
}
FrontAlign normalizes custom token names into CSS variables automatically — a key without a -- prefix gets one added; a key that already has it is left untouched.
Dark Mode Tokens
theme.dark provides dark-mode values for any of the recognized theme keys (body, bodyText, primary, primaryContrast, font, fontMono, spaceXs–space2xl). For custom tokens added via theme.extend, use the matching theme.dark.extend to override them in dark mode:
export default {
theme: {
body: '#ffffff',
bodyText: '#111827',
extend: {
'brand-surface': '#f8fafc',
'brand-border': '#e2e8f0'
},
dark: {
body: '#0f172a',
bodyText: '#e5e7eb',
extend: {
'brand-surface': '#111827',
'brand-border': '#334155'
}
}
}
}
This compiles into:
:root {
--body: #ffffff;
--body-text: #111827;
--brand-surface: #f8fafc;
--brand-border: #e2e8f0;
}
[fa-theme="dark"] {
--body: #0f172a;
--body-text: #e5e7eb;
--brand-surface: #111827;
--brand-border: #334155;
}
A key placed directly under
theme.darkthat isn't one of the recognized theme keys is silently dropped — it must go undertheme.dark.extendinstead. In the example above, writing'brand-surface': '#111827'as a direct sibling ofbodyinsidedark(instead of insidedark.extend) would not produce any output for it.
Breakpoints
Responsive breakpoints are also part of theme, defined under theme.breakpoints, right alongside colors, fonts, and spacing. FrontAlign ships with the following defaults:
export const DEFAULT_BREAKPOINTS = {
sm: '640px',
md: '864px',
lg: '1120px',
xl: '1408px',
'2xl': '1792px'
};
Override any tier by setting it under theme.breakpoints:
export default {
theme: {
primary: '#2563eb',
breakpoints: {
sm: '480px',
md: '768px',
lg: '1024px',
xl: '1280px',
'2xl': '1536px'
}
}
}
Any tier left out of theme.breakpoints falls back to DEFAULT_BREAKPOINTS — you don't have to redefine every key. The compiler turns each tier into a min-width media query, and only emits the ones actually used by scanned classes (md:hero-card, lg:hidden, and so on):
@media (min-width: 480px) {
/* sm utilities */
}
@media (min-width: 768px) {
/* md utilities */
}
@media (min-width: 1024px) {
/* lg utilities */
}
Like every other key inside theme, changing breakpoints while npx frontalign dev is running triggers a rebuild of frontalign.build.css with the new media query values — no restart required.
breakpointscan also be set at the top level of the config (export default { breakpoints: {...} }) instead of undertheme. If both are present,theme.breakpointstakes priority.
Custom Classes
The compiler generates custom classes defined in frontalign.config.js. All classes entries — object form or raw-string form — compile into the custom layer, regardless of how they're written:
export default {
classes: {
'hero-card': {
'background': 'var(--brand-surface)',
'border': '1px solid var(--brand-border)',
'border-radius': '1rem',
'padding': '2rem'
}
}
}
Generated CSS:
@layer custom {
.hero-card {
background: var(--brand-surface);
border: 1px solid var(--brand-border);
border-radius: 1rem;
padding: 2rem;
}
}
Class rules can also be defined as raw CSS declaration strings:
export default {
classes: {
'text-gradient': 'background: linear-gradient(90deg, #2563eb, #60a5fa); -webkit-background-clip: text; color: transparent;'
}
}
Dark Mode Custom Classes
Custom classes can include dark mode overrides:
export default {
classes: {
'hero-card': {
'background': '#ffffff',
'color': '#111827',
dark: {
'background': '#111827',
'color': '#e5e7eb'
}
}
}
}
Generated CSS:
@layer custom {
.hero-card {
background: #ffffff;
color: #111827;
}
[fa-theme="dark"] .hero-card {
background: #111827;
color: #e5e7eb;
}
}
Font Generation
FrontAlign generates Google Font imports and font utility classes from your config:
export default {
fonts: [
{
family: 'Inter',
weights: '400;500;600;700',
alias: 'inter',
category: 'sans-serif'
}
]
}
Generated utility class — compiled into the custom layer, alongside your classes entries:
@layer custom {
.font-inter {
font-family: 'Inter', sans-serif;
}
}
Usage:
<div class="font-inter">FrontAlign typography</div>
If alias is omitted, it is derived automatically from the family name — for example "DM Sans" becomes font-dm-sans. If category is omitted, it defaults to sans-serif.
CSS Variable Optimization
FrontAlign tracks CSS variables used inside scanned files, generated config CSS, base styles, and extracted utility CSS.
When JIT scanning is active, the compiler removes unused :root variables from the final output while keeping variables that are actually required.
It also resolves nested variable dependencies. For example, if your code uses:
color: var(--button-text);
and --button-text references another variable:
--button-text: var(--primary);
FrontAlign keeps both variables because the second is required by the first.
Component-Layer Purge Fallback
FrontAlign purges the components layer with one extra rule beyond exact matching: if a root class (e.g. card) is directly used, scanned, or safelisted, any component-layer selector containing a class that is a hyphenated sub-part of it (e.g. .card-header, .card-active) is kept automatically — even if that exact sub-class never appears literally in your source files. This exists because such classes are often only ever set dynamically via JS (classList, innerHTML, setAttribute) that no static scanner can see.
This fallback intentionally does not apply to the utilities layer: there, every class must match exactly, so using flex never silently keeps unrelated flex-1 or flex-wrap rules alive — utilities are atomic and independent, so the same fallback there would break utility purging almost entirely.
This only covers hyphenated class names on the same selector — a descendant selector like
.card .iconstill requiresiconto be used or safelisted on its own. Classes appearing only inside:not(),:has(),:is(), or:where()are treated as conditions, not requirements, and never gate whether a rule is kept.
Keyframes Purging
FrontAlign also purges unused @keyframes blocks automatically. It scans every animation and animation-name declaration across the base, component, and utility layers, collects the animation names actually referenced, and keeps only the @keyframes definitions that match — dropping the rest from the final output. No extra configuration is required.
Cache System
FrontAlign uses a local cache to speed up repeated builds.
The cache stores per-file scan data including:
- File modification time (
mtime) - File hash
- Extracted classes
- Extracted CSS variables
The cache is stored at:
node_modules/.cache/frontalign/cache.json
When a file has not changed, FrontAlign reuses the cached extraction result instead of re-scanning it.
When frontalign.css changes during dev mode, the cache is automatically invalidated and a full rebuild runs.
Ignored Paths
FrontAlign ignores common project folders that should not be scanned:
| Pattern | Reason |
|---|---|
/node_modules/ | Dependency files should not be scanned. |
/.git/ | Git internals are not source files. |
Build Pipeline
The full compiler pipeline runs in this order:
frontalign.config.js
↓
Load FrontAlign's bundled CSS (theme / base / components / utilities / keyframes inventory)
↓
Load cache & scan project files
↓
Extract classes and CSS variables per file
↓
Build theme AST (root & dark tokens, extend, dark.extend, fonts, classes → custom layer)
↓
Resolve breakpoints
↓
Filter active component & utility rules (JIT purge, incl. hyphen-prefix fallback for components)
↓
Filter active @keyframes by referenced animation names
↓
Resolve CSS variable dependency chains & filter unused root/dark variables
↓
Merge and organize into layers (PostCSS): theme → base → components → utilities → custom
↓
Apply breakpoint media queries
↓
Optimize production CSS (build mode only)
↓
Write public/frontalign.build.css
The PostCSS step merges :root and [fa-theme="dark"] blocks, removes duplicates, and normalizes token order in the final output. The layering step that follows is what guarantees custom always wins over utilities at equal specificity, no matter what order the source rules were written in.
Runtime Configuration for CDN Usage
For CDN projects or static pages where a build step is not used, FrontAlign also provides a runtime configuration API.
<script src="https://cdn.jsdelivr.net/npm/frontalign/dist/js/frontalign.min.js"></script>
<script>
new FrontAlign({
theme: {
primary: '#2563eb',
body: '#ffffff',
bodyText: '#111827',
dark: {
body: '#0f172a',
bodyText: '#e5e7eb'
}
},
classes: {
'hero-card': {
'background': 'var(--body)',
'color': 'var(--body-text)',
'padding': '2rem'
}
},
fonts: [
{
family: 'Inter',
weights: '400;500;600;700',
alias: 'inter'
}
]
});
</script>
The runtime engine can:
- Inject light and dark theme variables
- Generate custom class rules
- Generate font utility classes
- Inject Google Font stylesheet links
- Add preconnect hints for Google Fonts
Responsive breakpoints (theme.breakpoints) are a build-time feature and are not available through the runtime configuration API — media queries require the JIT scan step to know which tiers are actually used, which only runs during npx frontalign build or npx frontalign dev.
Compiler vs Runtime Configuration
| Feature | Compiler Engine | Runtime Configuration |
|---|---|---|
| Best for | NPM, Next.js, React, production apps | CDN, static HTML, quick prototypes |
| Output | public/frontalign.build.css | Injected |
| Scans project files | Yes | No |
| Extracts used utilities | Yes | No |
| Generates theme variables | Yes | Yes |
| Generates custom classes | Yes | Yes |
| Generates font utilities | Yes | Yes |
| Generates responsive breakpoints | Yes | No |
| Production optimization | Yes | No build step |
| Recommended for deployment | Yes | CDN / no-build workflows only |
Automatic JIT Dependency Installation
FrontAlign's JIT engine relies on a handful of packages under the hood — postcss, cssnano, glob, and chokidar — but they are not bundled with the framework itself. The CLI checks for them automatically the first time you run npx frontalign build or npx frontalign dev, and installs anything missing before continuing, so you never have to install them by hand.
| Package | Used for |
|---|---|
postcss | Parsing and transforming the compiled CSS AST |
cssnano | Production minification (npx frontalign build only) |
glob | Resolving jit.scan patterns to actual files |
chokidar | File watching in npx frontalign dev |
How detection works
- FrontAlign detects your package manager from the lockfile in your project root —
pnpm-lock.yaml→ pnpm,yarn.lock→ yarn, otherwise npm. - It verifies that manager's executable actually resolves on your system. If the lockfile points to a manager that isn't installed, FrontAlign silently falls back to npm.
- If a required package is missing, only that specific package is installed — not the full list — whenever Node can identify exactly which one is missing. If it can't, all four are installed together.
📦 FrontAlign: Installing missing JIT package(s) with npm: chokidar
After install
Because Node's ESM loader caches a failed import() for a given package even after it's installed moments later, FrontAlign does not retry the import in the same process. Instead, it re-runs the CLI command in a fresh Node process so module resolution starts clean.
If no supported package manager (npm, yarn, or pnpm) can be found on your system, or the automatic install itself fails, FrontAlign stops and prints the exact manual install command instead of retrying in a loop:
npm install -D postcss cssnano glob chokidar
Recommended Setup
For production applications, use the Compiler Engine:
bash npx frontalign build For development:
bash npx frontalign dev For CDN-only projects, use new FrontAlign({...}) runtime configuration.