Zero Runtime
tastyStatic is a build-time utility for generating CSS with zero runtime overhead. It's designed for static sites, no-JS websites, and performance-critical applications where you want to eliminate all runtime styling code. For the broader docs map, see the Docs Hub.
When to Use
- Static site generation (SSG) — Pre-render all styles at build time
- No-JavaScript websites — CSS works without any JS runtime
- Performance-critical pages — Zero runtime overhead for styling
- Landing pages — Minimal bundle size with pre-generated CSS
Requirements
The zero-runtime mode is part of the main @tenphi/tasty package but requires additional peer dependencies depending on your setup:
| Dependency | Version | Required for |
|---|---|---|
@babel/core | >= 7.24 | Babel plugin (@tenphi/tasty/babel-plugin) |
@babel/helper-plugin-utils | >= 7.24 | Babel plugin |
@babel/types | >= 7.24 | Babel plugin |
jiti | >= 2.6 | Next.js wrapper (@tenphi/tasty/next) when using configFile option |
All of these are declared as optional peer dependencies of @tenphi/tasty. Install only what your setup requires:
# For any Babel-based setup (Vite, custom Babel config, etc.)
pnpm add -D @babel/core @babel/helper-plugin-utils @babel/types
# For Next.js with TypeScript config file
pnpm add -D @babel/core @babel/helper-plugin-utils @babel/types jiti
Quick Start
Basic Usage
import { tastyStatic } from '@tenphi/tasty/static';
// Define styles - returns StaticStyle object
const button = tastyStatic({
display: 'inline-flex',
padding: '2x 4x',
fill: '#purple',
color: '#white',
radius: '1r',
});
// Use in JSX - works via toString() coercion
<button className={button}>Click me</button>
// Or access className explicitly
<button className={button.className}>Click me</button>
API Reference
tastyStatic(styles)
Creates a StaticStyle object from a styles definition.
const card = tastyStatic({
padding: '4x',
fill: '#white',
border: true,
radius: true,
});
tastyStatic(base, styles)
Extends an existing StaticStyle with additional styles. Uses mergeStyles internally for proper nested selector handling.
const button = tastyStatic({
padding: '2x 4x',
fill: '#blue',
Icon: { color: '#white' },
});
const primaryButton = tastyStatic(button, {
fill: '#purple',
Icon: { opacity: 0.8 },
});
tastyStatic(selector, styles)
Generates global styles for a CSS selector. The call is removed from the bundle after transformation.
tastyStatic('body', {
fill: '#surface',
color: '#text',
preset: 't3',
});
StaticStyle Object
| Property | Type | Description |
|---|---|---|
className | string | Space-separated class names for use in JSX |
styles | Styles | The original (or merged) styles object |
toString() | () => string | Returns className for string coercion |
Babel Plugin Configuration
Basic Configuration
// babel.config.js
module.exports = {
plugins: [
['@tenphi/tasty/babel-plugin', {
output: 'public/tasty.css',
}]
]
};
These examples use data-schema="dark" as the root-state convention. If your app already uses a different root attribute such as data-theme, keep the same alias pattern and swap the attribute name consistently in your zero-runtime config.
With Configuration
module.exports = {
plugins: [
['@tenphi/tasty/babel-plugin', {
output: 'public/tasty.css',
config: {
states: {
'@mobile': '@media(w < 768px)',
'@tablet': '@media(w < 1024px)',
'@dark': '@root(schema=dark)',
},
devMode: true,
},
}]
]
};
Plugin Options
| Option | Type | Default | Description |
|---|---|---|---|
output | string | 'tasty.css' | Path for generated CSS file |
mode | 'file' | 'inject' | 'file' | 'file' writes CSS to disk; 'inject' embeds CSS inline in JS (see Inject Mode) |
configFile | string | — | Absolute path to a TS/JS module that default-exports a TastyZeroConfig object. JSON-serializable alternative to config — required for Turbopack. |
config | TastyZeroConfig | () => TastyZeroConfig | {} | Inline config object or factory function. Takes precedence over configFile. |
configDeps | string[] | [] | Absolute file paths that affect config (for cache invalidation) |
config.states | Record<string, string> | {} | Predefined state aliases |
config.devMode | boolean | false | Add source comments to CSS |
config.recipes | Record<string, RecipeStyles> | {} | Predefined style recipes |
config.fontFace | Record<string, FontFaceInput> | — | Global @font-face definitions |
config.counterStyle | Record<string, CounterStyleDescriptors> | — | Global @counter-style definitions |
Recipes
Recipes work with tastyStatic the same way as with runtime tasty:
// babel.config.js
module.exports = {
plugins: [
['@tenphi/tasty/babel-plugin', {
output: 'public/tasty.css',
config: {
recipes: {
card: { padding: '4x', fill: '#surface', radius: '1r', border: true },
elevated: { shadow: '2x 2x 4x #shadow' },
},
},
}]
]
};
import { tastyStatic } from '@tenphi/tasty/static';
const card = tastyStatic({
recipe: 'card elevated',
color: '#text',
});
<div className={card}>Styled card</div>
Next.js Integration
The withTastyZero wrapper configures both webpack and Turbopack automatically. No --webpack flag is needed — it works with whichever bundler Next.js uses.
// next.config.ts
import { withTastyZero } from '@tenphi/tasty/next';
export default withTastyZero({
output: 'public/tasty.css',
configFile: './app/tasty-zero.config.ts',
configDeps: ['./app/theme.ts'],
})({
reactStrictMode: true,
});
withTastyZero Options
| Option | Type | Default | Description |
|---|---|---|---|
output | string | 'public/tasty.css' | Output path for CSS relative to project root |
mode | 'file' | 'inject' | 'file' | 'file' writes CSS to disk; 'inject' embeds CSS inline in JS |
enabled | boolean | true | Enable/disable the plugin |
configFile | string | — | Path to a TS/JS module that default-exports TastyZeroConfig. Recommended for Turbopack compatibility. |
config | TastyZeroConfig | — | Inline config object. For static configs that don't change during dev. |
configDeps | string[] | [] | Extra files the config depends on (for cache invalidation) |
Turbopack Support
Starting with Next.js 16, Turbopack is the default bundler. withTastyZero supports it out of the box by injecting turbopack.rules with babel-loader and JSON-serializable options.
The configFile option is key for Turbopack — it passes a file path (JSON-serializable) instead of a function, and the Babel plugin loads the config internally via jiti.
Requirements: babel-loader must be installed in your project:
pnpm add babel-loader
CSS Injection
withTastyZero automatically injects the generated CSS into your app. Every file that imports from @tenphi/tasty/static gets its import replaced with an import of the output CSS file at build time. No manual CSS import is needed.
The generated CSS file (e.g. public/tasty.css) is created as an empty stub before the first build if it doesn't exist, so there's no chicken-and-egg problem with fresh clones or CI builds. Add it to .gitignore:
public/tasty.css
Vite Integration
// vite.config.ts
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [
react({
babel: {
plugins: [
['@tenphi/tasty/babel-plugin', {
output: 'public/tasty.css',
config: {
states: { '@mobile': '@media(w < 768px)' },
},
}],
],
},
}),
],
});
Build Transformation
Before (Source Code)
import { tastyStatic } from '@tenphi/tasty/static';
const button = tastyStatic({
padding: '2x 4x',
fill: '#purple',
color: '#white',
});
tastyStatic('.heading', { preset: 'h1' });
export const Button = () => <button className={button}>Click</button>;
After (Production Build)
const button = {
className: 'ts3f2a1b ts8c4d2e',
styles: { padding: '2x 4x', fill: '#purple', color: '#white' },
toString() { return this.className; }
};
export const Button = () => <button className={button}>Click</button>;
Generated CSS (tasty.css)
/* Generated by @tenphi/tasty/zero - DO NOT EDIT */
.ts3f2a1b.ts3f2a1b {
padding: 16px 32px;
}
.ts8c4d2e.ts8c4d2e {
background: #9370db;
color: #fff;
}
.heading.heading {
font-size: 2.5rem;
font-weight: 700;
line-height: 1.2;
}
Inject Mode
By default the Babel plugin writes CSS to a file (mode: 'file'). Inject mode (mode: 'inject') embeds CSS inline in your JavaScript and injects it at runtime via a tiny injector. No CSS file is produced.
This is ideal for reusable components, extensions, and libraries where consumers shouldn't need to manage an external CSS file.
How It Works
- The Babel plugin extracts CSS at build time (same pipeline as file mode).
- Instead of writing to a
.cssfile, the CSS is embedded as string literals in the JS output. - The
@tenphi/tasty/staticimport is rewritten to@tenphi/tasty/static/inject. - Each
tastyStaticcall becomes a self-contained expression that injects its CSS and evaluates to aStaticStyleobject.
Configuration
// babel.config.js
module.exports = {
plugins: [
['@tenphi/tasty/babel-plugin', {
mode: 'inject',
config: {
states: { '@mobile': '@media(w < 768px)' },
},
}]
]
};
With Next.js:
// next.config.ts
import { withTastyZero } from '@tenphi/tasty/next';
export default withTastyZero({
mode: 'inject',
configFile: './app/tasty-zero.config.ts',
})({
reactStrictMode: true,
});
When mode is 'inject', the output and injectImport options are ignored.
Build Transformation (inject mode)
Before:
import { tastyStatic } from '@tenphi/tasty/static';
const button = tastyStatic({
padding: '2x 4x',
fill: '#purple',
});
tastyStatic('.heading', { preset: 'h1' });
After:
import { injectCSS as _$i } from '@tenphi/tasty/static/inject';
const button = (_$i("ts3f2a1b ts8c4d2e", ".ts3f2a1b.ts3f2a1b{padding:16px 32px}\n.ts8c4d2e.ts8c4d2e{background:#9370db}"), {
className: 'ts3f2a1b ts8c4d2e',
styles: { padding: '2x 4x', fill: '#purple' },
toString() { return this.className; }
});
_$i(".heading", ".heading{font-size:2.5rem;font-weight:700;line-height:1.2}");
Dev Mode / HMR
Class names are content-hashed (ts + MD5). When styles change, a new hash produces a new _$i call that injects fresh CSS. The injector deduplicates by id, so unchanged styles are skipped. Old CSS stays in the DOM but is harmless since no elements reference those class names.
Limitations (inject mode)
- Client-side only — Styles are injected via the DOM, so they are not available during SSR. For server-rendered apps, use
mode: 'file'or the runtimetasty(). - Larger JS bundle — CSS is embedded in JavaScript, increasing bundle size. Best suited for components and extensions, not full-app styling.
Style Extension
// Base button
const button = tastyStatic({
display: 'inline-flex',
padding: '2x 4x',
radius: '1r',
fill: '#gray.20',
color: '#text',
transition: 'fill 0.15s',
});
// Variants
const primaryButton = tastyStatic(button, {
fill: '#purple',
color: '#white',
});
const dangerButton = tastyStatic(button, {
fill: '#danger',
color: '#white',
});
State-based Styling
const card = tastyStatic({
padding: {
'': '4x',
'@mobile': '2x',
},
display: {
'': 'flex',
'@mobile': 'block',
},
});
Extending Style Types (TypeScript)
If you add custom style properties, use module augmentation so tastyStatic recognizes them too. See Extending Style Types in the configuration docs.
Limitations
- Static values only — All style values must be known at build time
- No runtime props — Cannot use
stylePropsor dynamicstylesprop - No mods at runtime — Modifiers must be defined statically
- Build-time transformation required — Babel plugin must process files
Workarounds
For dynamic styling needs, combine with regular CSS or CSS variables:
const card = tastyStatic({
padding: '4x',
fill: 'var(--card-bg, #white)',
});
<div
className={card}
style={{ '--card-bg': isActive ? '#purple' : '#white' }}
/>
Best Practices
- Define base styles for common patterns, then extend for variants
- Use selector mode for global/body styles
- Enable devMode in development for easier debugging
- Configure states for consistent responsive breakpoints
Common Issues
- No CSS file is generated: make sure the Babel plugin actually runs for files importing
@tenphi/tasty/static, and verify theoutputpath is writable. - Styles stay dynamic by mistake:
tastyStatic()only supports build-time-known values. Move runtime values to CSS variables or switch that component to runtimetasty(). - Turbopack config behaves inconsistently: prefer
configFileover inline functions so the setup stays JSON-serializable.
Related
- Docs Hub — Choose the right guide by task or rendering mode
- Style DSL — State maps, tokens, units, extending semantics (shared by runtime and static)
- Runtime API — Runtime styling:
tasty()factory, component props, variants, sub-elements, hooks - Configuration — Global configuration: tokens, recipes, custom units, and style handlers