Tasty logoTasty
Playground

Style Injector

A high-performance CSS-in-JS solution that powers the Tasty design system with efficient style injection, automatic cleanup, and first-class SSR support.


Overview

The Style Injector is the core engine behind Tasty's styling system, providing:

Note: This is internal infrastructure that powers Tasty components. Most developers will interact with the higher-level tasty() API instead.


Architecture

┌─────────────────┐    ┌──────────────────┐    ┌─────────────────┐
│   tasty()       │────│  Style Injector  │────│  Sheet Manager  │
│   components    │    │                  │    │                 │
└─────────────────┘    └──────────────────┘    └─────────────────┘
         │                        │                       │
         │                        │                       │
         ▼                        ▼                       ▼
┌─────────────────┐    ┌──────────────────┐    ┌─────────────────┐
│  Style Results  │    │ Keyframes Manager│    │  Root Registry  │
│  (CSS rules)    │    │                  │    │                 │
└─────────────────┘    └──────────────────┘    └─────────────────┘
         │                                            │
         │                                            │
         ▼                                            ▼
┌─────────────────┐                             ┌─────────────────┐
│   Hash Cache    │                             │ <style> elements│
│   Deduplication │                             │ CSSStyleSheet   │
└─────────────────┘                             └─────────────────┘

Core API

inject(rules, options?): InjectResult

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();

injectGlobal(rules, options?): { dispose: () => void }

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();

injectRawCSS(css, options?): { dispose: () => void }

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();

useRawCSS(css, options?) or useRawCSS(factory, deps, options?)

React hook for injecting raw CSS. Uses useInsertionEffect for proper timing and cleanup.

Supports two overloads:

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;
}

createInjector(config?): StyleInjector

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);

keyframes(steps, nameOrOptions?): KeyframesResult

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 DOM

configure(config): void

Configures 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:

Configuration Notes:


Advanced Features

Style Result Format

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,
};

Deduplication & Performance

// 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

Reference Counting

// 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 immediately

Smart Cleanup System

import { 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)

Shadow DOM Support

// 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' });

SSR & Testing

Server-Side Rendering

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);

Test Environment Detection

// 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
  });
});

Memory Management 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
});

Development Features

Performance Metrics

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
});

Debug Information

// 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
});

Cleanup History

// 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)
  });
});

Performance Optimizations

Best Practices

// ✅ 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
});

Memory Management

// 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 reset

Integration with Tasty

The 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 unmounts

For 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.


When to Use Direct Injection

Direct injector usage is recommended for:

For regular component styling, prefer the tasty() API which provides a more developer-friendly interface.