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.
scanner.utilitiesobjectCustom utility class syntax configuration. See Custom Utility Syntax.
scanner.utilities.patternRegExp/_[a-zA-Z]…/gRegex with global flag to extract utility class candidates from file content.
scanner.utilities.parse(className: string) => ParsedUtility | nullBuilt-inDecomposes a class name string into structured parts (name, value, modifiers). Return null for non-matching strings.
scanner.utilities.selectorUtilitySelectorFndefaultUtilitySelectorFnGenerates a raw class name from its parts. Must be the inverse of parse.

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

By default, Styleframe utility class names start with an underscore _ and use colons : as separators. You can change this convention to match your project's needs.

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 ?? [];
                    },
                ],
            },
        }),
    ],
});

Custom Utility Syntax

The default _modifier:property:value format can be replaced entirely. This is useful when you want to adopt a different naming convention — such as BEM, a Tailwind-like syntax, or a custom prefix that matches your team's standards.

The scanner's utilities config accepts three functions that control how class names are detected, decomposed, and reconstructed.

The Three Functions

FunctionPurposeInputOutput
patternFind class candidates in source filesRegExp with g flag
parseDecompose a class name into partsstringParsedUtility | null
selectorGenerate a class name from parts{ name, value, modifiers }string
parse and selector are inverses of each other. For any valid utility class, selector(parse(className)) should reproduce the original class name. The scanner uses parse to decompose detected classes, and the transpiler uses selector to generate CSS selectors.

Example: sf- Prefix with Dash Separators

This example replaces the default _modifier:property:value format with sf-modifier-property-value using dashes and an sf- prefix.

Define the shared selector function

Create a shared file so both configs use the same function:

selector.ts
// Generates class names like: sf-margin-sm, sf-hover-background-primary
export const selectorFn = ({ name, value, modifiers }) => {
    const parts = [
        ...modifiers,
        name,
        ...(value === 'default' ? [] : [value]),
    ].filter(Boolean);
    
    return `sf-${parts.join('-')}`;
};

Configure the scanner in your Vite config

Provide all three functions — pattern to find sf- candidates, parse to split them into parts, and selector to reconstruct them:

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

export default defineConfig({
    plugins: [
        styleframe({
            scanner: {
                content: ['./src/**/*.{html,vue,jsx,tsx}'],
                utilities: {
                    // Match all sf- prefixed class candidates
                    pattern: /sf-[a-zA-Z][a-zA-Z0-9-]*/g,
                    // Decompose: "sf-hover-margin-sm" → { modifiers: ["hover"], name: "margin", value: "sm" }
                    parse: (className) => {
                        if (!className.startsWith('sf-')) return null;
                        const parts = className.slice(3).split('-');
                        
                        if (parts.length < 2) {
                            return {
                                raw: className,
                                name: parts[0],
                                value: 'default',
                                modifiers: [],
                                isArbitrary: false,
                            };
                        }
                        
                        const value = parts[parts.length - 1];
                        const name = parts[parts.length - 2];
                        const modifiers = parts.slice(0, -2);
                        
                        return {
                            raw: className,
                            name,
                            value,
                            modifiers,
                            isArbitrary: false,
                        };
                    },
                    selector: selectorFn,
                },
            },
        }),
    ],
});

Set the same selector on the Styleframe instance

The transpiler reads utilities.selector from the Styleframe instance to generate CSS selectors that match your class names:

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

const s = styleframe({
    utilities: {
        selector: selectorFn,
    },
});

useUtilitiesPreset(s);
useModifiersPreset(s);

export default s;

Use the custom class names in your templates

component.html
<div class="sf-display-flex sf-padding-1rem sf-gap-0.5rem">
    <button class="sf-background-primary sf-hover-background-secondary">
        Click me
    </button>
</div>
When you customize selector, you must set the same function on both the scanner config (scanner.utilities.selector) and the Styleframe instance (utilities.selector). If these differ, the generated CSS selectors will not match the class names in your markup.

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