CSS Support
CSS support in tsdown is still an experimental feature. While it covers the core use cases, the API and behavior may change in future releases.
Experimental Feature
CSS support is experimental. Please test thoroughly and report any issues you encounter. The API and behavior may change as the feature matures.
Getting Started
All CSS support in tsdown is provided by the @tsdown/css package. Install it to enable CSS handling:
npm install -D @tsdown/cssWhen @tsdown/css is installed, CSS processing is automatically enabled.
CSS Import
Importing .css files from your TypeScript or JavaScript entry points is supported. The CSS content is extracted and emitted as a separate .css asset file:
// src/index.ts
import './style.css'
export function greet() {
return 'Hello'
}This produces both index.mjs and index.css in the output directory.
@import Inlining
CSS @import statements are automatically resolved and inlined into the output. This means you can use @import to organize your CSS across multiple files without producing separate output files:
/* style.css */
@import './reset.css';
@import './theme.css';
.main {
color: red;
}All imported CSS is bundled into a single output file with @import statements removed.
Inline CSS (?inline)
Appending ?inline to a CSS import returns the fully processed CSS as a JavaScript string instead of emitting a separate .css file. This aligns with Vite's ?inline behavior:
import css from './theme.css?inline' // Returns processed CSS as a string
import './style.css' // Extracted to a .css file
console.log(css) // ".theme { color: red; }\n"The ?inline CSS goes through the full processing pipeline — preprocessors, @import inlining, syntax lowering, and minification — just like regular CSS. The only difference is the output format: a JavaScript string export instead of a CSS asset file.
This also works with preprocessors:
import css from './theme.scss?inline'When ?inline is used, the CSS is not included in the emitted .css files and the import is tree-shakeable (moduleSideEffects: false).
CSS Pre-processors
tsdown provides built-in support for .scss, .sass, .less, .styl, and .stylus files. The corresponding pre-processor must be installed as a dev dependency:
# Either sass-embedded (recommended, faster) or sass
npm install -D sass-embedded
# or
npm install -D sassnpm install -D lessnpm install -D stylusOnce installed, you can import preprocessor files directly:
import './style.scss'
import './theme.less'
import './global.styl'Preprocessor Options
You can pass options to each preprocessor via css.preprocessorOptions:
export default defineConfig({
css: {
preprocessorOptions: {
scss: {
additionalData: `$brand-color: #ff7e17;`,
},
less: {
math: 'always',
},
},
},
})additionalData
Each preprocessor supports an additionalData option to inject extra code at the beginning of every processed file. This is useful for global variables or mixins:
export default defineConfig({
css: {
preprocessorOptions: {
scss: {
// String — prepended to every .scss file
additionalData: `@use "src/styles/variables" as *;`,
},
},
},
})You can also use a function for dynamic injection:
export default defineConfig({
css: {
preprocessorOptions: {
scss: {
additionalData: (source, filename) => {
if (filename.includes('theme')) return source
return `@use "src/styles/variables" as *;\n${source}`
},
},
},
},
})CSS Minification
Enable CSS minification via css.minify:
export default defineConfig({
css: {
minify: true,
},
})Minification is powered by Lightning CSS.
CSS Target
By default, CSS syntax lowering uses the top-level target option. You can override this specifically for CSS with css.target:
export default defineConfig({
target: 'node18',
css: {
target: 'chrome90', // CSS-specific target
},
})Set css.target: false to disable CSS syntax lowering entirely, even when a top-level target is set:
export default defineConfig({
target: 'chrome90',
css: {
target: false, // Preserve modern CSS syntax
},
})CSS Transformer
The css.transformer option controls how CSS is processed. PostCSS and Lightning CSS are mutually exclusive processing paths:
'lightningcss'(default):@importis resolved by Lightning CSS'sbundleAsync(), and PostCSS is not used at all.'postcss':@importis resolved bypostcss-import, PostCSS plugins are applied, then Lightning CSS is used only for final syntax lowering and minification.
export default defineConfig({
css: {
transformer: 'postcss', // Use PostCSS for @import and plugins
},
})When using the 'postcss' transformer, install postcss and optionally postcss-import for @import resolution:
npm install -D postcss postcss-importPostCSS Options
Configure PostCSS inline or point to a config file:
export default defineConfig({
css: {
transformer: 'postcss',
postcss: {
plugins: [require('autoprefixer')],
},
},
})Or specify a directory path to search for a PostCSS config file (postcss.config.js, etc.):
export default defineConfig({
css: {
transformer: 'postcss',
postcss: './config', // Search for postcss.config.js in ./config/
},
})When css.postcss is omitted and transformer is 'postcss', tsdown auto-detects PostCSS config from the project root.
Lightning CSS
tsdown uses Lightning CSS for CSS syntax lowering — transforming modern CSS features into syntax compatible with older browsers based on your target setting.
To enable CSS syntax lowering, install lightningcss:
npm install -D lightningcsspnpm add -D lightningcssyarn add -D lightningcssbun add -D lightningcssOnce installed, CSS lowering is enabled automatically when a target is set. For example, with target: 'chrome108', CSS nesting & selectors will be flattened:
/* Input */
.foo {
& .bar {
color: red;
}
}
/* Output (chrome108) */
.foo .bar {
color: red;
}Lightning CSS Options
You can pass additional options to Lightning CSS via css.lightningcss:
import { Features } from 'lightningcss'
export default defineConfig({
css: {
lightningcss: {
// Override browser targets directly (instead of using `target`)
targets: { chrome: 100 << 16 },
// Include/exclude specific features
include: Features.Nesting,
},
},
})TIP
When css.lightningcss.targets is set, it takes precedence over both the top-level target and css.target options for CSS transformations.
For more information on available options, refer to the Lightning CSS documentation.
Preserving CSS Imports (css.inject)
By default, CSS import statements are removed from JS output after extracting the CSS into separate files. When css.inject is enabled, the JS output preserves import statements pointing to the emitted CSS files, so consumers of your library will automatically import the CSS alongside the JS:
export default defineConfig({
css: {
inject: true,
},
})With css.inject: true, the output JS will contain:
// dist/index.mjs
import './style.css'
export function greet() {
return 'Hello'
}This is useful for component libraries where you want CSS to be automatically included when users import your components.
CSS Modules
Files with the .module.css extension (and preprocessor variants like .module.scss, .module.less, etc.) are treated as CSS modules. Class names are automatically scoped and exported as a JavaScript object:
// src/index.ts
import styles from './app.module.css'
console.log(styles.title) // "scoped_title_hash"/* app.module.css */
.title {
color: red;
}
.content {
font-size: 14px;
}The CSS is emitted with scoped class names, and the JS output exports the mapping from original to scoped names.
Configuration
Configure CSS modules behavior via css.modules:
export default defineConfig({
css: {
modules: {
// Scoping behavior: 'local' (default) or 'global'
scopeBehaviour: 'local',
// Pattern for scoped class names (Lightning CSS pattern syntax)
generateScopedName: '[hash]_[local]',
// Transform class name convention in JS exports
localsConvention: 'camelCase',
},
},
})Set css.modules: false to disable CSS modules entirely — .module.css files will be treated as regular CSS.
localsConvention
Controls how class names are exported in JavaScript:
| Value | Input | Exports |
|---|---|---|
| (not set) | foo-bar | foo-bar |
'camelCase' | foo-bar | foo-bar, fooBar |
'camelCaseOnly' | foo-bar | fooBar |
'dashes' | foo-bar | foo-bar, fooBar |
'dashesOnly' | foo-bar | fooBar |
generateScopedName
When using transformer: 'lightningcss' (default), this accepts a Lightning CSS pattern string (e.g., '[hash]_[local]').
When using transformer: 'postcss', this also accepts a function:
export default defineConfig({
css: {
transformer: 'postcss',
modules: {
generateScopedName: (name, filename, css) => {
return `my-lib_${name}`
},
},
},
})NOTE
Function-form generateScopedName is only supported with transformer: 'postcss'. The Lightning CSS transformer only supports string patterns.
Optional Dependencies
When using transformer: 'postcss' with CSS modules, install postcss-modules:
npm install -D postcss postcss-modulesCSS Code Splitting
Merged Mode (Default)
By default, all CSS is merged into a single file (default: style.css):
dist/
index.mjs
style.css ← all CSS mergedCustom File Name
You can customize the merged CSS file name:
export default defineConfig({
css: {
fileName: 'my-library.css',
},
})Splitting Mode
To split CSS per chunk — so each JavaScript chunk that imports CSS has a corresponding .css file — enable splitting:
export default defineConfig({
css: {
splitting: true,
},
})dist/
index.mjs
index.css ← CSS from index.ts
async-abc123.mjs
async-abc123.css ← CSS from async chunkPostCSS Optional Peer Dependencies
When using transformer: 'postcss', the following packages may need to be installed depending on the features you use:
| Package | Purpose | Required When |
|---|---|---|
postcss | Core PostCSS engine | Always (with transformer: 'postcss') |
postcss-import | Resolve and inline @import statements | CSS files use @import |
postcss-modules | CSS modules support (scoped class names) | Using .module.css files |
npm install -D postcss postcss-import postcss-modulesAll three are declared as optional peer dependencies of @tsdown/css and only loaded when needed.
Options Reference
| Option | Type | Default | Description |
|---|---|---|---|
css.transformer | 'postcss' | 'lightningcss' | 'lightningcss' | CSS processing pipeline |
css.splitting | boolean | false | Enable CSS code splitting per chunk |
css.fileName | string | 'style.css' | File name for the merged CSS file (when splitting: false) |
css.minify | boolean | false | Enable CSS minification |
css.modules | object | false | {} | CSS modules configuration, or false to disable |
css.target | string | string[] | false | from target | CSS-specific syntax lowering target |
css.postcss | string | object | — | PostCSS config path or inline options |
css.preprocessorOptions | object | — | Options for CSS preprocessors |
css.inject | boolean | false | Preserve CSS import statements in JS output |
css.lightningcss | object | — | Options passed to Lightning CSS for syntax lowering |