---
url: /options/css.md
---
# 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.

> \[!WARNING] 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:

```bash
npm install -D @tsdown/css
```

When `@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:

```ts
// 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:

```css
/* 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](https://vite.dev/guide/features#disabling-css-injection-into-the-page):

```ts
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:

```ts
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:

::: code-group

```sh [Sass]
# Either sass-embedded (recommended, faster) or sass
npm install -D sass-embedded
# or
npm install -D sass
```

```sh [Less]
npm install -D less
```

```sh [Stylus]
npm install -D stylus
```

:::

Once installed, you can import preprocessor files directly:

```ts
import './style.scss'
import './theme.less'
import './global.styl'
```

### Preprocessor Options

You can pass options to each preprocessor via `css.preprocessorOptions`:

```ts
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:

```ts
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:

```ts
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`:

```ts
export default defineConfig({
  css: {
    minify: true,
  },
})
```

Minification is powered by [Lightning CSS](https://lightningcss.dev/).

## CSS Target

By default, CSS syntax lowering uses the top-level [`target`](/options/target) option. You can override this specifically for CSS with `css.target`:

```ts
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:

```ts
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): `@import` is resolved by Lightning CSS's `bundleAsync()`, and PostCSS is **not used at all**.
* **`'postcss'`**: `@import` is resolved by [`postcss-import`](https://github.com/postcss/postcss-import), PostCSS plugins are applied, then Lightning CSS is used only for final syntax lowering and minification.

```ts
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:

```bash
npm install -D postcss postcss-import
```

### PostCSS Options

Configure PostCSS inline or point to a config file:

```ts
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.):

```ts
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](https://lightningcss.dev/) 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`:

::: code-group

```sh [npm]
npm install -D lightningcss
```

```sh [pnpm]
pnpm add -D lightningcss
```

```sh [yarn]
yarn add -D lightningcss
```

```sh [bun]
bun add -D lightningcss
```

:::

Once installed, CSS lowering is enabled automatically when a `target` is set. For example, with `target: 'chrome108'`, CSS nesting `&` selectors will be flattened:

```css
/* 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`:

```ts
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](https://lightningcss.dev/).

## Preserving CSS Imports (`css.inject`) {#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:

```ts
export default defineConfig({
  css: {
    inject: true,
  },
})
```

With `css.inject: true`, the output JS will contain:

```js
// 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](https://github.com/css-modules/css-modules). Class names are automatically scoped and exported as a JavaScript object:

```ts
// src/index.ts
import styles from './app.module.css'

console.log(styles.title) // "scoped_title_hash"
```

```css
/* 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`:

```ts
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`            |

You can also provide a function to generate a custom export name. It receives
the original class name, generated scoped class name, and input file:

```ts
export default defineConfig({
  css: {
    modules: {
      localsConvention: (originalClassName, generatedClassName, inputFile) => {
        return originalClassName.replaceAll(/-([a-z0-9])/g, (_, character) =>
          character.toUpperCase(),
        )
      },
    },
  },
})
```

The function form works with both CSS transformers. It changes the exported
JavaScript key only; `generatedClassName` remains the value of that export.

### `generateScopedName`

When using `transformer: 'lightningcss'` (default), this accepts a Lightning CSS [pattern string](https://lightningcss.dev/css-modules.html#custom-naming-conventions) (e.g., `'[hash]_[local]'`).

When using `transformer: 'postcss'`, this also accepts a function:

```ts
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`](https://github.com/css-modules/postcss-modules):

```bash
npm install -D postcss postcss-modules
```

## CSS Code Splitting

### Merged Mode (Default)

By default, all CSS is merged into a single file (default: `style.css`). The exception is [unbundle mode](/options/unbundle), where `css.splitting` defaults to `true` to preserve the module structure:

```
dist/
  index.mjs
  style.css  ← all CSS merged
```

### Custom File Name

You can customize the merged CSS file name:

```ts
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:

```ts
export default defineConfig({
  css: {
    splitting: true,
  },
})
```

```
dist/
  index.mjs
  index.css        ← CSS from index.ts
  async-abc123.mjs
  async-abc123.css ← CSS from async chunk
```

## PostCSS 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`](https://github.com/postcss/postcss)                     | Core PostCSS engine                      | Always (with `transformer: 'postcss'`) |
| [`postcss-import`](https://github.com/postcss/postcss-import)       | Resolve and inline `@import` statements  | CSS files use `@import`                |
| [`postcss-modules`](https://github.com/css-modules/postcss-modules) | CSS modules support (scoped class names) | Using `.module.css` files              |

```bash
npm install -D postcss postcss-import postcss-modules
```

All 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` (`true` if `unbundle`) | 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         |
