Webpack loader that dynamically injects CSS into the DOM at runtime with multiple injection strategies and CSS modules support
—
CSS Modules support with automatic export generation, named exports, and lazy loading integration.
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}`;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 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 stylesUsage 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}`;
}
}
}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// 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"
}
}
}
],
},
],
},
};Install with Tessl CLI
npx tessl i tessl/npm-style-loader