Gracile — Standard CSS Modules (Vite plugin)
Use import attributes in Vite to
get a
CSSStyleSheet
or a Lit CSSResult from your CSS
files.
// Standard CSS module — returns a CSSStyleSheet (client) or CSSResult (SSR)
import styles from './my-element.css' with { type: 'css' };
// Lit-specific — always returns a CSSResult
import styles from './my-element.css' with { type: 'css-lit' };
Works with CSS, SCSS, Sass, Less, Stylus, and PostCSS — anything Vite can process.
Caution
Experimental. This add-on is under active development and its API may change.
Concept
The CSS Module Scripts proposal
lets you import .css files as constructable CSSStyleSheet objects using
import … with { type: 'css' }. Browser support is growing but bundlers don’t
handle it natively yet.
This Vite plugin bridges the gap: it rewrites CSS import attributes at build
time so you can use the standard syntax today — with full support for Vite’s CSS
pipeline (PostCSS, Sass, Less, etc.) and automatic SSR fallback via Lit’s
unsafeCSS().
Installation
npm i vite-plugin-standard-css-modules
Peer dependencies
vite(5.x, 6.x, 7.x, or 8.x)lit(3.x) — optional, required only when usingtype: 'css-lit'or SSR
Setup
Add the plugin to your Vite (or Astro, Gracile, SvelteKit, etc.) config:
// vite.config.ts
import { standardCssModulesfunction standardCssModules(options?: Options): Plugin } from 'vite-plugin-standard-css-modules';
export default {
pluginsplugins: Plugin<any>[] : [standardCssModulesfunction standardCssModules(options?: Options): Plugin ()],
};
Usage
type: 'css' — Constructable stylesheet
On the client, the import is transformed into a
constructable CSSStyleSheet
populated via replaceSync():
import styles from './my-element.css' with { type: 'css' };
class MyElement extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
this.shadowRoot.adoptedStyleSheets = [styles];
}
}
During SSR, CSSStyleSheet doesn’t exist in Node.js. The plugin
automatically falls back to Lit’s unsafeCSS(), returning a CSSResult instead
— no config needed.
type: 'css-lit' — Lit CSSResult
Always returns a Lit CSSResult, on both client and server. Use this when you
want a CSSResult everywhere (e.g. for Lit’s static styles):
import { LitElement, html } from 'lit';
import styles from './my-element.css' with { type: 'css-lit' };
class MyElement extends LitElement {
static styles = [styles];
render() {
return html`<p>Hello</p>`;
}
}
Lit handles both CSSStyleSheet and CSSResult in static styles, so you can
mix type: 'css' and type: 'css-lit' imports freely.
Options
standardCssModules({
include: ['**/*.{js,jsx,ts,tsx,mjs,mts,cjs,cts}'], // default
exclude: ['**/node_modules/**'], // default
outputMode: undefined, // default — auto-detect per import
log: false, // default
});
| Option | Type | Description |
|---|---|---|
include | string[] | Glob patterns for JS/TS files to transform. |
exclude | string[] | Glob patterns to skip. |
outputMode | 'CSSStyleSheet' | 'CSSResult' | Force every CSS import to produce a specific output, overriding per-import type attributes and SSR auto-detection. |
log | boolean | Print each transformed import to the console. |
outputMode
By default the plugin decides per import:
type: 'css'on the client →CSSStyleSheettype: 'css'during SSR →CSSResult(automatic fallback)type: 'css-lit'→CSSResulteverywhere
Setting outputMode overrides this globally:
// Always emit CSSResult (Lit's unsafeCSS), even on the client
standardCssModules({ outputMode: 'CSSResult' });
// Always emit CSSStyleSheet, even during SSR
// (make sure a CSSStyleSheet polyfill is available server-side)
standardCssModules({ outputMode: 'CSSStyleSheet' });
Pre/post-processors
All of Vite’s CSS pipelines work out of the box. Import SCSS, Sass, Less, Stylus, or PostCSS files with the same syntax:
import tokensmodule "*.scss" from './tokens.scss' with { type: 'css' };
import thememodule "*.less" from './theme.less' with { type: 'css-lit' };
TypeScript / IDE awareness
Add a triple-slash reference so your editor resolves CSS default imports as
CSSStyleSheet:
// src/vite-env.d.ts (or src/env.d.ts for Astro)
/// <reference types="vite-plugin-standard-css-modules/css-modules" />
/// <reference types="vite/client" />
See css-modules.d.ts for the full list of declared extensions.
How it works
- The plugin’s
transformhook runs withenforce: 'pre'. - OXC’s parser (
oxc-parser) scans the file’s AST forImportDeclarationnodes with awith { type: 'css' | 'css-lit' }attribute on a CSS-like file. - Each matching import is rewritten using
magic-string:- The CSS specifier gets a
?inlinequery appended so Vite processes it through its normal CSS pipeline (PostCSS, Sass, etc.) and returns the final CSS string. - For client +
type: 'css': anew CSSStyleSheet()is created and populated withreplaceSync(). - For
type: 'css-lit'or SSR:unsafeCSS()fromlitwraps the string into aCSSResult.
- The CSS specifier gets a
- A
handleHotUpdatehook invalidates JS modules when their imported CSS files change.