A high-performance CSS-in-JS solution that powers the Tasty design system with efficient style injection, automatic cleanup, and first-class SSR support.
The Style Injector is the core engine behind Tasty's styling system, providing:
- Hash-based deduplication - Identical CSS gets the same className
- Reference counting - Automatic cleanup when components unmount (refCount = 0)
- CSS nesting flattening - Handles
&,.Class,SubElementpatterns - Keyframes injection - First-class
@keyframessupport with immediate disposal - Smart cleanup - CSS rules batched cleanup, keyframes disposed immediately
- SSR support - Deterministic class names and CSS extraction
- Multiple roots - Works with Document and ShadowRoot
- Non-stacking cleanups - Prevents timeout accumulation for better performance
Note: This is internal infrastructure that powers Tasty components. Most developers will interact with the higher-level
tasty()API instead.
┌─────────────────┐ ┌──────────────────┐ ┌─────────────────┐
│ tasty() │────│ Style Injector │────│ Sheet Manager │
│ components │ │ │ │ │
└─────────────────┘ └──────────────────┘ └─────────────────┘
│ │ │
│ │ │
▼ ▼ ▼
┌─────────────────┐ ┌──────────────────┐ ┌─────────────────┐
│ Style Results │ │ Keyframes Manager│ │ Root Registry │
│ (CSS rules) │ │ │ │ │
└─────────────────┘ └──────────────────┘ └─────────────────┘
│ │
│ │
▼ ▼
┌─────────────────┐ ┌─────────────────┐
│ Hash Cache │ │ <style> elements│
│ Deduplication │ │ CSSStyleSheet │
└─────────────────┘ └─────────────────┘
Injects CSS rules and returns a className with dispose function.
import { inject } from '@tenphi/tasty';
// Component styling - generates tasty class names
const result = inject([{
selector: '.t-abc123',
declarations: 'color: red; padding: 10px;',
}]);
console.log(result.className); // 't-abc123'
// Cleanup when component unmounts (refCount decremented)
result.dispose();Injects global styles that don't reserve tasty class names.
// Global styles - for body, resets, etc.
const globalResult = injectGlobal([
{
selector: 'body',
declarations: 'margin: 0; font-family: Arial;',
},
{
selector: '.header',
declarations: 'background: blue; color: white;',
atRules: ['@media (min-width: 768px)'],
}
]);
// Only returns dispose function - no className needed for global styles
globalResult.dispose();Injects raw CSS text directly without parsing. This is a low-overhead method for injecting CSS that doesn't need tasty processing.
import { injectRawCSS } from '@tenphi/tasty';
// Inject raw CSS
const { dispose } = injectRawCSS(`
body {
margin: 0;
padding: 0;
font-family: sans-serif;
}
.my-class {
color: red;
}
`);
// Later, remove the injected CSS
dispose();React hook for injecting raw CSS. Uses useInsertionEffect for proper timing and cleanup.
Supports two overloads:
- Static CSS:
useRawCSS(cssString, options?) - Factory function:
useRawCSS(() => cssString, deps, options?)- re-evaluates when deps change (likeuseMemo)
import { useRawCSS } from '@tenphi/tasty';
// Static CSS
function GlobalReset() {
useRawCSS(`
body { margin: 0; padding: 0; }
`);
return null;
}
// Dynamic CSS with factory function (like useMemo)
function ThemeStyles({ theme }: { theme: 'dark' | 'light' }) {
useRawCSS(() => `
body {
margin: 0;
background: ${theme === 'dark' ? '#000' : '#fff'};
color: ${theme === 'dark' ? '#fff' : '#000'};
}
`, [theme]);
return null;
}Creates an isolated injector instance with custom configuration.
import { createInjector } from '@tenphi/tasty';
// Create isolated instance for testing
const testInjector = createInjector({
devMode: true,
forceTextInjection: true,
});
const result = testInjector.inject(rules);Injects CSS keyframes with automatic deduplication.
// Generated name (k0, k1, k2...)
const fadeIn = keyframes({
from: { opacity: 0 },
to: { opacity: 1 },
});
// Custom name
const slideIn = keyframes({
'0%': { transform: 'translateX(-100%)' },
'100%': { transform: 'translateX(0)' },
}, 'slideInAnimation');
// Use in tasty styles (recommended)
const AnimatedBox = tasty({
styles: {
animation: `${fadeIn} 300ms ease-in`,
},
});
// Or use with injectGlobal for fixed selectors
injectGlobal([{
selector: '.my-animated-class',
declarations: `animation: ${slideIn} 500ms ease-out;`
}]);
// Cleanup keyframes (if needed)
fadeIn.dispose(); // Immediate keyframes deletion from DOM
slideIn.dispose(); // Immediate keyframes deletion from DOMConfigures the Tasty style system. configure() is optional, but if you use it, it must be called before any styles are generated (before first render).
import { configure } from '@tenphi/tasty';
configure({
devMode: true, // Enable development features (auto-detected)
maxRulesPerSheet: 8192, // Cap rules per stylesheet (default: 8192)
unusedStylesThreshold: 500, // Trigger cleanup threshold (CSS rules only)
bulkCleanupDelay: 5000, // Cleanup delay (ms) - ignored if idleCleanup is true
idleCleanup: true, // Use requestIdleCallback for cleanup
bulkCleanupBatchRatio: 0.5, // Clean up oldest 50% per batch
unusedStylesMinAgeMs: 10000, // Minimum age before cleanup (ms)
forceTextInjection: false, // Force textContent insertion (auto-detected for tests)
nonce: 'csp-nonce', // CSP nonce for security
states: { // Global predefined states for advanced state mapping
'@mobile': '@media(w < 768px)',
'@dark': '@root(schema=dark)',
},
});Auto-Detection Features:
devMode: Automatically enabled in development environments (detected viaisDevEnv())forceTextInjection: Automatically enabled in test environments (Jest, Vitest, Mocha, jsdom)
Configuration Notes:
- Most options have sensible defaults and auto-detection
configure()is optional - the injector works with defaults- Configuration is locked after styles are generated - calling
configure()after first render will emit a warning and be ignored unusedStylesMinAgeMs: Minimum time (ms) a style must remain unused before being eligible for cleanup. Helps prevent removal of styles that might be quickly reactivated.
The injector works with StyleResult objects from the tasty parser:
interface StyleResult {
selector: string; // CSS selector
declarations: string; // CSS declarations
atRules?: string[]; // @media, @supports, etc.
nestingLevel?: number; // Nesting depth for specificity
}
// Example StyleResult
const styleRule: StyleResult = {
selector: '.t-button',
declarations: 'padding: 8px 16px; background: blue; color: white;',
atRules: ['@media (min-width: 768px)'],
nestingLevel: 0,
};// Identical CSS rules get the same className
const button1 = inject([{
selector: '.t-btn1',
declarations: 'padding: 8px; color: red;'
}]);
const button2 = inject([{
selector: '.t-btn2',
declarations: 'padding: 8px; color: red;' // Same declarations
}]);
// Both get the same className due to deduplication
console.log(button1.className === button2.className); // true// Multiple components using the same styles
const comp1 = inject([commonStyle]);
const comp2 = inject([commonStyle]);
const comp3 = inject([commonStyle]);
// Style is kept alive while any component uses it
comp1.dispose(); // refCount: 3 → 2
comp2.dispose(); // refCount: 2 → 1
comp3.dispose(); // refCount: 1 → 0, eligible for bulk cleanup
// Rule exists but refCount = 0 means unused
// Next inject() with same styles will increment refCount and reuse immediatelyimport { configure } from '@tenphi/tasty';
// CSS rules: Not immediately deleted, marked for bulk cleanup (refCount = 0)
// Keyframes: Disposed immediately when refCount = 0 (safer for global scope)
configure({
unusedStylesThreshold: 100, // Cleanup when 100+ unused CSS rules
bulkCleanupBatchRatio: 0.3, // Remove oldest 30% each time
});
// Benefits:
// - CSS rules: Batch cleanup prevents DOM manipulation overhead
// - Keyframes: Immediate cleanup prevents global namespace pollution
// - Unused styles can be instantly reactivated (just increment refCount)// Works with Shadow DOM
const shadowRoot = document.createElement('div').attachShadow({ mode: 'open' });
const shadowStyles = inject([{
selector: '.shadow-component',
declarations: 'color: purple;'
}], { root: shadowRoot });
// Keyframes in Shadow DOM
const shadowAnimation = keyframes({
from: { opacity: 0 },
to: { opacity: 1 }
}, { root: shadowRoot, name: 'shadowFade' });import { getCssText, getCssTextForNode } from '@tenphi/tasty';
// Extract all CSS for SSR
const cssText = getCssText();
// Extract CSS for specific DOM subtree (like jest-styled-components)
const container = render(<MyComponent />);
const componentCSS = getCssTextForNode(container);// Automatically detected test environments:
// - NODE_ENV === 'test'
// - Jest globals (jest, describe, it, expect)
// - jsdom user agent
// - Vitest globals (vitest)
// - Mocha globals (mocha)
import { configure, isTestEnvironment, resetConfig } from '@tenphi/tasty';
const isTest = isTestEnvironment();
// Reset config between tests to allow reconfiguration
beforeEach(() => {
resetConfig();
configure({
forceTextInjection: isTest, // More reliable in test environments
devMode: true, // Always enable dev features in tests
});
});// Clean up between tests
afterEach(() => {
cleanup(); // Force cleanup of unused styles
});
// Full cleanup after test suite
afterAll(() => {
destroy(); // Destroy all stylesheets and reset state
});When devMode is enabled, the injector tracks comprehensive metrics:
import { configure, injector } from '@tenphi/tasty';
configure({ devMode: true });
// Access metrics through the global injector
const metrics = injector.instance.getMetrics();
console.log({
cacheHits: metrics.hits, // Successful cache hits
cacheMisses: metrics.misses, // New styles injected
unusedHits: metrics.unusedHits, // Current unused styles (calculated on demand)
bulkCleanups: metrics.bulkCleanups, // Number of bulk cleanup operations
stylesCleanedUp: metrics.stylesCleanedUp, // Total styles removed in bulk cleanups
totalInsertions: metrics.totalInsertions, // Lifetime insertions
totalUnused: metrics.totalUnused, // Total styles marked as unused (refCount = 0)
startTime: metrics.startTime, // Metrics collection start timestamp
cleanupHistory: metrics.cleanupHistory, // Detailed cleanup operation history
});// Get detailed information about injected styles
const debugInfo = injector.instance.getDebugInfo();
console.log({
activeStyles: debugInfo.activeStyles, // Currently active styles
unusedStyles: debugInfo.unusedStyles, // Styles marked for cleanup
totalSheets: debugInfo.totalSheets, // Number of stylesheets
totalRules: debugInfo.totalRules, // Total CSS rules
});// Track cleanup operations over time
const metrics = injector.instance.getMetrics();
metrics.cleanupHistory.forEach(cleanup => {
console.log({
timestamp: new Date(cleanup.timestamp),
classesDeleted: cleanup.classesDeleted,
rulesDeleted: cleanup.rulesDeleted,
cssSize: cleanup.cssSize, // Total CSS size removed (bytes)
});
});// ✅ Reuse styles - identical CSS gets deduplicated
const buttonBase = { padding: '8px 16px', borderRadius: '4px' };
// ✅ Avoid frequent disposal and re-injection
// Let the reference counting system handle cleanup
// ✅ Use bulk operations for global styles
injectGlobal([
{ selector: 'body', declarations: 'margin: 0;' },
{ selector: '*', declarations: 'box-sizing: border-box;' },
{ selector: '.container', declarations: 'max-width: 1200px;' }
]);
// ✅ Configure appropriate thresholds for your app (BEFORE first render!)
import { configure } from '@tenphi/tasty';
configure({
unusedStylesThreshold: 500, // Default threshold (adjust based on app size)
bulkCleanupBatchRatio: 0.5, // Default: clean oldest 50% per batch
unusedStylesMinAgeMs: 10000, // Wait 10s before cleanup eligibility
});// The injector automatically manages memory through:
// 1. Hash-based deduplication - same CSS = same className
// 2. Reference counting - styles stay alive while in use (refCount > 0)
// 3. Immediate keyframes cleanup - disposed instantly when refCount = 0
// 4. Batch CSS cleanup - unused CSS rules (refCount = 0) cleaned in batches
// 5. Non-stacking cleanups - prevents timeout accumulation
// Manual cleanup is rarely needed but available:
cleanup(); // Force immediate cleanup of all unused CSS rules (refCount = 0)
destroy(); // Nuclear option: remove all stylesheets and resetThe Style Injector is seamlessly integrated with the higher-level Tasty API:
// High-level tasty() API
const StyledButton = tasty({
styles: {
padding: '2x 4x',
fill: '#purple',
color: '#white',
}
});
// Internally uses the injector:
// 1. Styles are parsed into StyleResult objects
// 2. inject() is called with the parsed results
// 3. Component gets the returned className
// 4. dispose() is called when component unmountsFor most development, you'll use the Runtime API rather than the injector directly. The injector provides the high-performance foundation that makes Tasty's declarative styling possible.
Direct injector usage is recommended for:
- Custom CSS-in-JS libraries built on top of Tasty
- Global styles that don't fit the component model
- Third-party integration where you need low-level CSS control
- Performance-critical scenarios where you need direct control
- Testing utilities that need to inject or extract CSS
For regular component styling, prefer the tasty() API which provides a more developer-friendly interface.