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. |
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
All Styleframe utility class names start with an underscore _ and use colons : as separators.
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 ?? [];
},
],
},
}),
],
});
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 }).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.