Tooling

Utility Scanner

Automatically detect utility class names in your source files and generate the corresponding CSS at build time — no manual value definitions needed.

The scanner reads your project's source files, finds Styleframe utility class names, and auto-generates the corresponding CSS. You write classes in your templates, and the scanner handles CSS generation at build time — similar to Tailwind CSS JIT mode.

Why Use the Scanner?

  • Zero manual registration: Use utility classes directly in your markup without pre-defining every value
  • Automatic CSS generation: Only the CSS you use gets generated
  • Arbitrary value support: Use bracket syntax like _padding:[2.5rem] for one-off values
  • Modifier auto-detection: Compound class names like _hover:background:primary are recognized and registered automatically
  • HMR support: Template changes trigger incremental rescans during development
  • Framework-agnostic: Built-in extractors for HTML, Vue, React, Svelte, Solid, Astro, MDX, and more

Setup

The scanner needs registered utility and modifier factories to match against. Use useUtilitiesPreset() and useModifiersPreset() to register all built-in factories, or register individual ones as needed.

Enable the scanner in your Vite config

Add the scanner option with content glob patterns to the Styleframe plugin:

vite.config.ts
import { defineConfig } from 'vite';
import styleframe from '@styleframe/plugin/vite';

export default defineConfig({
    plugins: [
        styleframe({
            scanner: {
                // Glob patterns for files to scan
                content: ['./src/**/*.{html,vue,jsx,tsx,svelte,astro}'],
            },
        }),
    ],
});

Register utility and modifier factories

Open your styleframe.config.ts and register the factories the scanner will match against:

styleframe.config.ts
import { styleframe } from 'styleframe';
import { useUtilitiesPreset } from '@styleframe/theme';
import { useModifiersPreset } from '@styleframe/theme';

const s = styleframe();

// Register utility factories for the scanner to match against
useUtilitiesPreset(s);

// Register modifier factories for modifier detection
useModifiersPreset(s);

export default s;

Use utility classes in your templates

Write utility classes directly in your markup. The scanner detects them and generates the CSS automatically.

component.html
<!-- Use utility classes directly — the scanner generates the CSS -->
<div class="_display:flex _padding:1rem _gap:0.5rem">
    <p class="_font-size:1.25rem _color:primary">Hello world</p>
    <button class="_background:primary _hover:background:secondary _padding:0.75rem">
        Click me
    </button>
</div>

Configuration

The scanner is configured through the scanner option in the Vite plugin.

OptionTypeDefaultDescription
scanner.contentstring[]Glob patterns for files to scan for utility class names. Required.
scanner.extractorsExtractor[]Built-inCustom extractor functions for file types not supported by default. Each extractor receives (content: string, filePath: string) and returns an array of class name strings.

Content Patterns

Specify which files the scanner should search using glob patterns:

vite.config.ts
styleframe({
    scanner: {
        content: [
            './src/**/*.{html,vue,jsx,tsx}',       // Source files
            './components/**/*.svelte',            // Svelte components
            './pages/**/*.astro',                  // Astro pages
            './content/**/*.mdx',                  // MDX content
        ],
    },
})

Default Ignore Patterns

The scanner automatically skips these directories:

  • **/node_modules/**
  • **/.git/**
  • **/dist/**
  • **/build/**
  • **/.next/**
  • **/.nuxt/**
  • **/coverage/**

Class Name Format

All Styleframe utility class names start with an underscore _ and use colons : as separators.

Basic Utilities

Format: _name:value

Class NameUtilityValue
_margin:smmarginsm
_display:flexdisplayflex
_hiddenhiddendefault

With Modifiers

Format: _modifier:name:value or _mod1:mod2:name:value

Class NameModifiersUtilityValue
_hover:background:primaryhoverbackgroundprimary
_dark:hover:background:primarydark, hoverbackgroundprimary
_sm:margin:lgsmmarginlg

Arbitrary Values

Format: _name:[css-value]

Class NameUtilityCSS Value
_padding:[2.5rem]padding2.5rem
_background:[#1E3A8A]background#1E3A8A
_margin:[10px_20px]margin10px 20px
Pro tip: Use underscores _ in place of spaces within brackets. For example, _margin:[10px_20px] generates margin: 10px 20px.

How It Works

The scanner processes your files in five steps:

1. Extraction

The scanner reads your source files and extracts all strings matching the _name:value pattern. Each file type has a specialized extractor optimized for its syntax.

2. Parsing

Each extracted class name is parsed into a structured representation:

// "_hover:background:primary" is parsed as:
{
    raw: "_hover:background:primary",
    name: "background",
    value: "primary",
    modifiers: ["hover"],
    isArbitrary: false,
}

3. Matching

Parsed classes are matched against registered utility and modifier factories on the Styleframe root instance. A class like _hover:background:primary requires both a background utility factory and a hover modifier factory to be registered.

4. Registration

For matched utilities that don't already exist:

  • Token values (e.g., _margin:sm): The factory's autogenerate function is called with @sm to resolve the design token
  • Arbitrary values (e.g., _padding:[2.5rem]): The factory's create method is called with the literal CSS value 2.5rem
  • Modifiers: Detected modifier factories are passed to the utility registration and merged when duplicate utility+value pairs appear across files

5. CSS Generation

The registered utilities are transpiled to CSS alongside all other Styleframe declarations and served through the virtual:styleframe.css module.

Supported File Types

ExtensionExtraction Strategy
.html, .htmclass="..." attributes
.vueTemplate class bindings + <script> string literals
.svelteclass attributes + class:_directive syntax + <script>
.jsx, .tsxclassName="..." + className={...} expressions
.js, .tsString literals (single, double, and template)
.astroHTML + JSX + frontmatter
.mdxHTML + JSX patterns
.php, .erb, .twigHTML attribute extraction
.blade.phpHTML attribute extraction (Laravel Blade)

Framework Examples

Button.tsx
export function Button({ children }) {
    return (
        <button className="_padding:md _background:primary _hover:background:secondary _font-weight:bold">
            {children}
        </button>
    );
}

Custom Extractors

For file types not in the default list, provide a custom extractor function. An extractor receives the file content and path, and returns an array of class name strings.

vite.config.ts
import { defineConfig } from 'vite';
import styleframe from '@styleframe/plugin/vite';

export default defineConfig({
    plugins: [
        styleframe({
            scanner: {
                content: ['./src/**/*.mytpl'],
                extractors: [
                    (content, filePath) => {
                        if (!filePath.endsWith('.mytpl')) return [];

                        // Extract all underscore-prefixed class names
                        const matches = content.match(
                            /_[a-zA-Z][a-zA-Z0-9-]*(?::[a-zA-Z0-9._-]+|\[[^\]]+\])*/g
                        );
                        return matches ?? [];
                    },
                ],
            },
        }),
    ],
});

Caching

The scanner uses content-hash-based caching to avoid re-scanning unchanged files. When a file is scanned, its content is hashed and stored alongside the result. On subsequent scans, the hash is compared before re-processing.

During development with HMR, only changed files are re-scanned. The Vite plugin invalidates the cache for the changed file and rescans it incrementally. If new utility values are registered, the CSS is regenerated.

Best Practices

  • Be specific with glob patterns: Use precise patterns like ./src/**/*.tsx instead of ./**/* to avoid scanning unnecessary files
  • Register all needed factories: The scanner can only match against registered factories — use useUtilitiesPreset() and useModifiersPreset() for comprehensive coverage
  • Prefer design tokens over arbitrary values: While _padding:[2.5rem] works, _padding:lg with a design token is more consistent and maintainable
  • Avoid dynamic class names: The scanner performs static analysis and cannot detect runtime-constructed strings like `_margin:${size}`
  • Pre-register dynamic values: If you need dynamic class names, define them explicitly in your config rather than relying on scanner detection
  • Exclude non-production files: Keep test files and fixtures out of your content patterns to avoid generating unused CSS

FAQ