CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/npm-style-loader

Webpack loader that dynamically injects CSS into the DOM at runtime with multiple injection strategies and CSS modules support

Pending
Overview
Eval results
Files

css-modules.mddocs/

CSS Modules Integration

CSS Modules support with automatic export generation, named exports, and lazy loading integration.

Capabilities

CSS Modules Exports

When CSS Modules are enabled (via css-loader), style-loader automatically generates JavaScript exports for CSS class names.

/**
 * CSS Modules exports interface
 * Default export contains all class name mappings
 * Named exports provide individual class access
 */
interface CSSModuleExports {
  [className: string]: string;  // Named exports for each class
  default?: Record<string, string>;  // Default export with all classes
}

Usage Example:

/* component.module.css */
.header {
  color: blue;
  font-size: 24px;
}

.button {
  background: #007bff;
  border: none;
  padding: 8px 16px;
}

.active {
  background: #28a745;
}
// webpack.config.js
module.exports = {
  module: {
    rules: [
      {
        test: /\.module\.css$/i,
        use: [
          "style-loader",
          {
            loader: "css-loader",
            options: { modules: true }
          }
        ],
      },
    ],
  },
};

// component.js - Default import
import styles from "./component.module.css";
console.log(styles.header);  // "component_header__1a2b3c"
console.log(styles.button);  // "component_button__4d5e6f"

// component.js - Named imports
import { header, button, active } from "./component.module.css";
const element = document.createElement("div");
element.className = `${header} ${button} ${active}`;

Lazy CSS Modules

When using lazy injection types, CSS Modules exports are extended with use() and unuse() methods.

/**
 * Lazy CSS Modules exports interface
 * Extends standard CSS Modules with lazy loading controls
 */
interface LazyCSSModuleExports extends CSSModuleExports {
  /**
   * Activate lazy-loaded styles
   * @param insertOptions - Optional runtime insertion configuration
   * @returns Self for chaining
   */
  use(insertOptions?: InsertOptions): LazyCSSModuleExports;
  
  /**
   * Deactivate lazy-loaded styles
   * Uses reference counting - styles removed when count reaches 0
   */
  unuse(): void;
  
  /** CSS Modules class name mappings (also available as named exports) */
  locals?: Record<string, string>;
}

interface InsertOptions {
  insertInto?: HTMLElement;
  insertAt?: "top" | "bottom" | number;
}

Usage Example:

/* modal.lazy.css */
.modal {
  position: fixed;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  background: white;
  border-radius: 8px;
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
}

.overlay {
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  background: rgba(0, 0, 0, 0.5);
}
// webpack.config.js
module.exports = {
  module: {
    rules: [
      {
        test: /\.lazy\.css$/i,
        use: [
          { 
            loader: "style-loader", 
            options: { injectType: "lazyStyleTag" }
          },
          {
            loader: "css-loader",
            options: { modules: true }
          }
        ],
      },
    ],
  },
};

// Modal component
import modalStyles from "./modal.lazy.css";

class Modal {
  constructor() {
    this.isOpen = false;
  }
  
  open() {
    if (!this.isOpen) {
      // Activate modal styles when opening
      modalStyles.use();
      
      // Create modal elements with CSS module classes
      this.overlay = document.createElement("div");
      this.overlay.className = modalStyles.overlay;
      
      this.modal = document.createElement("div");
      this.modal.className = modalStyles.modal;
      
      document.body.appendChild(this.overlay);
      document.body.appendChild(this.modal);
      
      this.isOpen = true;
    }
  }
  
  close() {
    if (this.isOpen) {
      // Remove modal elements
      document.body.removeChild(this.overlay);
      document.body.removeChild(this.modal);
      
      // Deactivate modal styles when closing
      modalStyles.unuse();
      
      this.isOpen = false;
    }
  }
}

Named Exports with Lazy Loading

Named exports work seamlessly with lazy loading capabilities.

// Both default and named imports support lazy loading
import lazyStyles, { className1, className2 } from "./styles.lazy.css";

// All these references point to the same lazy loading controls
lazyStyles.use();        // Activate styles
className1.use();        // Same as above - all refer to same module
lazyStyles.unuse();      // Deactivate styles

Usage Example:

// theme-switcher.js
import lightTheme, { header, button } from "./light-theme.lazy.css";
import darkTheme from "./dark-theme.lazy.css";

class ThemeSwitcher {
  constructor() {
    this.currentTheme = "light";
    lightTheme.use(); // Activate default theme
  }
  
  switchTheme(theme) {
    // Deactivate current theme
    if (this.currentTheme === "light") {
      lightTheme.unuse();
    } else {
      darkTheme.unuse();
    }
    
    // Activate new theme
    if (theme === "light") {
      lightTheme.use();
    } else {
      darkTheme.use();
    }
    
    this.currentTheme = theme;
  }
  
  applyClasses(element) {
    // Named exports are available even when styles are lazy
    if (this.currentTheme === "light") {
      element.className = `${header} ${button}`;
    } else {
      element.className = `${darkTheme.header} ${darkTheme.button}`;
    }
  }
}

Reference Counting

Lazy CSS modules use reference counting to manage activation/deactivation safely.

/**
 * Reference counting behavior for lazy CSS modules
 * - use() increments reference count and activates styles on first call
 * - unuse() decrements reference count and deactivates styles when count reaches 0
 * - Multiple use() calls require matching unuse() calls
 */

Usage Example:

import styles from "./component.lazy.css";

// Component A uses styles
styles.use();  // Ref count: 1, styles activated

// Component B also uses the same styles  
styles.use();  // Ref count: 2, styles remain active

// Component A unmounts
styles.unuse(); // Ref count: 1, styles remain active

// Component B unmounts
styles.unuse(); // Ref count: 0, styles deactivated and removed

CSS Modules Configuration Examples

// Complete CSS Modules setup with different injection types

// Standard CSS Modules (automatic injection)
module.exports = {
  module: {
    rules: [
      {
        test: /\.module\.css$/i,
        use: [
          "style-loader",
          {
            loader: "css-loader",
            options: {
              modules: {
                localIdentName: "[name]_[local]__[hash:base64:5]"
              }
            }
          }
        ],
      },
    ],
  },
};

// Lazy CSS Modules for conditional loading
module.exports = {
  module: {
    rules: [
      {
        test: /\.lazy\.css$/i,
        use: [
          { 
            loader: "style-loader", 
            options: { injectType: "lazyStyleTag" }
          },
          {
            loader: "css-loader",
            options: {
              modules: {
                localIdentName: "[name]_[local]__[hash:base64:5]",
                exportLocalsConvention: "camelCase"
              }
            }
          }
        ],
      },
    ],
  },
};

Best Practices

  1. Use named imports for better tree shaking and IDE support
  2. Lazy load large stylesheets that aren't always needed
  3. Match use/unuse calls to prevent memory leaks
  4. Use reference counting wisely in component-based applications
  5. Configure localIdentName for readable class names in development

Install with Tessl CLI

npx tessl i tessl/npm-style-loader

docs

css-modules.md

index.md

injection-types.md

loader-configuration.md

runtime-api.md

tile.json