Zero-runtime atomic CSS framework for vanilla-extract that generates static utility classes with type-safe composition
—
Transform property definitions into type-safe utility functions that generate CSS class names. Supports both build-time and runtime usage patterns.
Creates a type-safe function for accessing your defined properties. You can provide as many collections of properties as you like.
/**
* Creates a type-safe function for accessing defined properties
* @param config - Variable number of property configuration objects from defineProperties
* @returns SprinklesFn with static properties Set
*/
function createSprinkles<Args extends ReadonlyArray<SprinklesProperties>>(
...config: Args
): SprinklesFn<Args>;
/**
* The sprinkles function type with properties introspection
*/
type SprinklesFn<Args extends ReadonlyArray<SprinklesProperties>> = ((
props: SprinkleProps<Args>
) => string) & { properties: Set<keyof SprinkleProps<Args>> };
/**
* Combined props type from all provided property configurations
*/
type SprinkleProps<Args extends ReadonlyArray<any>> = Args extends [
infer L,
...infer R
]
? (L extends SprinklesProperties ? ChildSprinkleProps<L['styles']> : never) &
SprinkleProps<R>
: {};The sprinkles function exposes a static properties key that lets you check whether a given property can be handled by the function.
/**
* Static property set for introspection
*/
sprinkles.properties: Set<keyof SprinkleProps<Args>>;
/**
* Check if a property is handled by the sprinkles function
*/
sprinkles.properties.has(propertyName: string): boolean;Basic sprinkles creation:
import { defineProperties, createSprinkles } from "@vanilla-extract/sprinkles";
const responsiveProperties = defineProperties({
conditions: {
mobile: {},
tablet: { '@media': 'screen and (min-width: 768px)' },
desktop: { '@media': 'screen and (min-width: 1024px)' }
},
defaultCondition: 'mobile',
properties: {
display: ['none', 'flex', 'block'],
flexDirection: ['row', 'column'],
padding: {
small: '8px',
medium: '16px',
large: '24px'
}
}
});
const colorProperties = defineProperties({
conditions: {
lightMode: {},
darkMode: { '@media': '(prefers-color-scheme: dark)' }
},
defaultCondition: 'lightMode',
properties: {
color: {
primary: '#007bff',
secondary: '#6c757d'
},
background: {
surface: '#ffffff',
elevated: '#f8f9fa'
}
}
});
// Create sprinkles function from multiple property sets
export const sprinkles = createSprinkles(
responsiveProperties,
colorProperties
);Static usage in .css.ts files:
import { sprinkles } from './sprinkles.css';
// Basic static usage
export const container = sprinkles({
display: 'flex',
padding: 'medium',
color: 'primary'
});
// Conditional styling
export const responsiveContainer = sprinkles({
display: 'block',
flexDirection: {
mobile: 'column',
desktop: 'row'
},
background: {
lightMode: 'surface',
darkMode: 'elevated'
}
});
// Responsive array notation
export const spacedContainer = sprinkles({
display: 'flex',
padding: ['small', 'medium', 'large'] // mobile, tablet, desktop
});Runtime usage:
import { sprinkles } from './sprinkles.css';
// Dynamic runtime usage
function createDynamicStyles(isColumn: boolean, theme: 'light' | 'dark') {
return sprinkles({
display: 'flex',
flexDirection: isColumn ? 'column' : 'row',
background: theme === 'dark' ? 'elevated' : 'surface'
});
}
// Conditional runtime styling
const flexDirection = Math.random() > 0.5 ? 'column' : 'row';
const dynamicClass = sprinkles({
display: 'flex',
flexDirection
});Combining with vanilla-extract styles:
import { style } from '@vanilla-extract/css';
import { sprinkles } from './sprinkles.css';
// Combine sprinkles with custom styles
export const customContainer = style([
sprinkles({
display: 'flex',
padding: 'medium'
}),
{
':hover': {
outline: '2px solid currentColor'
},
'::before': {
content: '""',
display: 'block'
}
}
]);Using in vanilla-extract selectors:
import { globalStyle } from '@vanilla-extract/css';
import { sprinkles } from './sprinkles.css';
const cardContainer = sprinkles({
padding: 'medium',
background: 'surface'
});
// Sprinkles can be used in selectors
globalStyle(`${cardContainer} *`, {
boxSizing: 'border-box'
});
globalStyle(`${cardContainer}:hover`, {
transform: 'scale(1.02)'
});Properties introspection:
import { sprinkles } from './sprinkles.css';
// Check if property is supported
console.log(sprinkles.properties.has('padding')); // true
console.log(sprinkles.properties.has('fontSize')); // false
// Get all supported properties
const allProperties = Array.from(sprinkles.properties);
console.log(allProperties); // ['display', 'flexDirection', 'padding', 'color', 'background']
// Useful for building Box components
function Box({ children, ...props }) {
const sprinkleProps = {};
const otherProps = {};
for (const [key, value] of Object.entries(props)) {
if (sprinkles.properties.has(key)) {
sprinkleProps[key] = value;
} else {
otherProps[key] = value;
}
}
return (
<div className={sprinkles(sprinkleProps)} {...otherProps}>
{children}
</div>
);
}Error handling:
// Development-time error handling
try {
const invalidClass = sprinkles({
display: 'invalid-value' // Error: "display" has no value "invalid-value"
});
} catch (error) {
console.error(error.message); // SprinklesError with helpful message
}
try {
const noDefault = sprinkles({
color: { desktop: 'primary' } // Error if no default condition set
});
} catch (error) {
console.error(error.message); // Error about missing default condition
}The sprinkles function provides full type safety:
const sprinkles = createSprinkles(/* ... */);
// TypeScript will enforce valid property names and values
const validClass = sprinkles({
display: 'flex', // ✓ Valid
padding: 'medium', // ✓ Valid
flexDirection: {
mobile: 'column', // ✓ Valid condition and value
desktop: 'row' // ✓ Valid condition and value
}
});
// These would cause TypeScript errors:
const invalidClass = sprinkles({
display: 'invalid', // ✗ Invalid value
unknownProp: 'value', // ✗ Unknown property
flexDirection: {
invalidCondition: 'row' // ✗ Invalid condition
}
});Sprinkles provides runtime validation with detailed error messages in development mode:
/**
* Custom error class for sprinkles validation failures
*/
class SprinklesError extends Error {
constructor(message: string);
name: 'SprinklesError';
}Invalid property values:
try {
const className = sprinkles({
display: 'invalid-value' // Error: "display" has no value "invalid-value"
});
} catch (error) {
console.error(error.name); // 'SprinklesError'
console.error(error.message); // Detailed validation message
}Missing default condition:
// If defineProperties has defaultCondition: false
try {
const className = sprinkles({
display: 'flex' // Error: no default condition specified
});
} catch (error) {
console.error(error.message); // Must specify conditions explicitly
}Invalid conditions:
try {
const className = sprinkles({
display: {
invalidCondition: 'flex' // Error: unknown condition
}
});
} catch (error) {
console.error(error.message); // Condition validation error
}function safeSprinkles(props: any, fallback = '') {
try {
return sprinkles(props);
} catch (error) {
if (error.name === 'SprinklesError') {
console.warn('Sprinkles validation error:', error.message);
return fallback;
}
throw error; // Re-throw non-sprinkles errors
}
}
// Usage with graceful fallback
const className = safeSprinkles({
display: userInput, // Potentially invalid
padding: 'medium'
}, 'fallback-class');Install with Tessl CLI
npx tessl i tessl/npm-vanilla-extract--sprinkles