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.
- 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
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 jitiimport { 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>Creates a StaticStyle object from a styles definition.
const card = tastyStatic({
padding: '4x',
fill: '#white',
border: true,
radius: true,
});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 },
});Generates global styles for a CSS selector. The call is removed from the bundle after transformation.
tastyStatic('body', {
fill: '#surface',
color: '#text',
preset: 't3',
});| 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.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.
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,
},
}]
]
};| Option | Type | Default | Description |
|---|---|---|---|
output |
string |
'tasty.css' |
Path for generated CSS file |
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 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>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,
});| Option | Type | Default | Description |
|---|---|---|---|
output |
string |
'public/tasty.css' |
Output path for CSS relative to project root |
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) |
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-loaderwithTastyZero 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.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)' },
},
}],
],
},
}),
],
});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>;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 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;
}// 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',
});const card = tastyStatic({
padding: {
'': '4x',
'@mobile': '2x',
},
display: {
'': 'flex',
'@mobile': 'block',
},
});If you add custom style properties, use module augmentation so tastyStatic recognizes them too. See Extending Style Types in the configuration docs.
- 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
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' }}
/>- 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
- 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.
- 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