Utility Scanner
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:primaryare 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
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:
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:
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.
<!-- 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.
| Option | Type | Default | Description |
|---|---|---|---|
scanner.content | string[] | — | Glob patterns for files to scan for utility class names. Required. |
scanner.extractors | Extractor[] | Built-in | Custom 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.utilities | object | — | Custom utility class syntax configuration. See Custom Utility Syntax. |
scanner.utilities.pattern | RegExp | /_[a-zA-Z]…/g | Regex with global flag to extract utility class candidates from file content. |
scanner.utilities.parse | (className: string) => ParsedUtility | null | Built-in | Decomposes a class name string into structured parts (name, value, modifiers). Return null for non-matching strings. |
scanner.utilities.selector | UtilitySelectorFn | defaultUtilitySelectorFn | Generates 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:
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 Name | Utility | Value |
|---|---|---|
_margin:sm | margin | sm |
_display:flex | display | flex |
_hidden | hidden | default |
With Modifiers
Format: _modifier:name:value or _mod1:mod2:name:value
| Class Name | Modifiers | Utility | Value |
|---|---|---|---|
_hover:background:primary | hover | background | primary |
_dark:hover:background:primary | dark, hover | background | primary |
_sm:margin:lg | sm | margin | lg |
Arbitrary Values
Format: _name:[css-value]
| Class Name | Utility | CSS Value |
|---|---|---|
_padding:[2.5rem] | padding | 2.5rem |
_background:[#1E3A8A] | background | #1E3A8A |
_margin:[10px_20px] | margin | 10px 20px |
_ 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'sautogeneratefunction is called with@smto resolve the design token - Arbitrary values (e.g.,
_padding:[2.5rem]): The factory'screatemethod is called with the literal CSS value2.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
| Extension | Extraction Strategy |
|---|---|
.html, .htm | class="..." attributes |
.vue | Template class bindings + <script> string literals |
.svelte | class attributes + class:_directive syntax + <script> |
.jsx, .tsx | className="..." + className={...} expressions |
.js, .ts | String literals (single, double, and template) |
.astro | HTML + JSX + frontmatter |
.mdx | HTML + JSX patterns |
.php, .erb, .twig | HTML attribute extraction |
.blade.php | HTML attribute extraction (Laravel Blade) |
Framework Examples
export function Button({ children }) {
return (
<button className="_padding:md _background:primary _hover:background:secondary _font-weight:bold">
{children}
</button>
);
}
<template>
<button class="_padding:md _background:primary _hover:background:secondary _font-weight:bold">
<slot />
</button>
</template>
<button class="_padding:md _background:primary _hover:background:secondary _font-weight:bold">
<slot />
</button>
<!-- Svelte class directive syntax is also supported -->
<button class:_margin:sm={hasMargin}>
<slot />
</button>
<button class="_padding:md _background:primary _hover:background:secondary _font-weight:bold">
Click me
</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.
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
| Function | Purpose | Input | Output |
|---|---|---|---|
pattern | Find class candidates in source files | — | RegExp with g flag |
parse | Decompose a class name into parts | string | ParsedUtility | null |
selector | Generate 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:
// 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:
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:
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
<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>
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/**/*.tsxinstead of./**/*to avoid scanning unnecessary files - Register all needed factories: The scanner can only match against registered factories — use
useUtilitiesPreset()anduseModifiersPreset()for comprehensive coverage - Prefer design tokens over arbitrary values: While
_padding:[2.5rem]works,_padding:lgwith 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
_property:value with colons instead of shorthand names, and you have full control over auto-generation behavior through factory functions.useUtilitiesPreset() or an individual composable.@styleframe/scanner package provides a standalone programmatic API. Use createScanner() to integrate the scanner into any build tool or custom workflow.`_margin:${size}` cannot be resolved. For dynamic values, pre-register the utilities in your config file.extractors option. Your extractor receives the file content and path, and returns an array of class name strings. See the Custom Extractors section above for an example.className={...} expressions and extracts string literals from within braced expressions, including patterns like clsx({ '_margin:sm': true }).utilities config with pattern, parse, and selector functions to replace the default _modifier:property:value format. You must also set the same selector function on the Styleframe instance via utilities.selector. See Custom Utility Syntax for a full example.Overview
Explore the tools that integrate Styleframe into your development workflow — from automatic CSS generation at build time to bidirectional design token syncing with Figma.
Figma Plugin
Sync your Styleframe design tokens with Figma using the plugin and CLI commands for bidirectional token synchronization.