CSS Processing

Comprehensive CSS, Sass, Less, Stylus, PostCSS, and CSS Modules support in Packem

CSS Processing

Packem provides first-class support for CSS and all major CSS preprocessors. From vanilla CSS to advanced CSS Modules, Packem handles all your styling needs through the @visulima/rollup-plugin-css plugin.

CSS options are configured under rollup.css and are typed as StyleOptions from @visulima/rollup-plugin-css. The CSS loaders and minifiers ship with Packem and are imported from the @visulima/packem/css/loader/* and @visulima/packem/css/minifier/* sub-paths. Register a loader by adding it to the loaders array.

Supported CSS Technologies

CSS

Standard CSS with PostCSS processing and optimization

Sass/SCSS

Sass and SCSS preprocessing with Dart Sass or Embedded Sass

Less

Less preprocessing with full feature support

Stylus

Stylus preprocessing with expressive syntax

PostCSS

PostCSS with autoprefixer and plugin ecosystem

CSS Modules

Scoped CSS with automatic class name generation

Lightning CSS

Fast CSS parsing, transformation, and minification

cssnano

Advanced CSS minification and optimization

Basic CSS Support

Packem processes CSS files imported in your JavaScript/TypeScript code. Register the PostCSS loader to enable plain .css processing:

// src/index.ts
import './styles.css'

export function createButton() {
  const button = document.createElement('button')
  button.className = 'btn btn-primary'
  button.textContent = 'Click me'
  return button
}
/* src/styles.css */
.btn {
  padding: 0.5rem 1rem;
  border: none;
  border-radius: 0.25rem;
  cursor: pointer;
}

.btn-primary {
  background-color: #007bff;
  color: white;
}

Configuration

Configure CSS processing in your packem.config.ts. The PostCSS loader requires postcss to be installed.

import { defineConfig } from '@visulima/packem/config'
import transformer from '@visulima/packem/transformer/esbuild'
import postcssLoader from '@visulima/packem/css/loader/postcss'

export default defineConfig({
  transformer,
  rollup: {
    css: {
      mode: 'extract',
      loaders: [postcssLoader],
    },
  },
})

Available css options

The css option accepts the following keys (typed as StyleOptions):

  • mode — how CSS is delivered: "inject" (default), "extract", "emit", or "inline". See Output Modes.
  • loaders — array of CSS loaders to register (PostCSS, Sass, Less, Stylus, Lightning CSS, Tailwind CSS).
  • autoModules — enable CSS Modules for *.module.* files. Accepts a boolean, RegExp, or function (@default false).
  • minifier — a minifier object (cssnano or Lightning CSS). See Minification.
  • cssnano — options for the cssnano minifier.
  • lightningcss — options for Lightning CSS.
  • postcss — PostCSS options (plugins, modules, etc.).
  • sass / less / stylus — preprocessor loader options.
  • extensions — file extensions to process (@default [".css", ".pcss", ".postcss", ".sss"]).
  • include / exclude — file filters (RegExp, string, or array).
  • alias — URL and import path aliases.
  • sourceMap — source map control: boolean, "inline", or a tuple with SourceMapOptions (@default false).
  • dts — generate .d.ts files for style imports.
  • namedExports — export each class as a named binding (@default false).
  • onImport / onExtract — lifecycle callbacks.

Sass/SCSS

Packem supports both Sass and SCSS syntax with Dart Sass or Embedded Sass.

Installation

npm install --save-dev sass
npm install --save-dev sass-embedded

Usage

// src/styles.scss
$primary-color: #007bff;
$border-radius: 0.25rem;

@mixin button-style($bg-color) {
  background-color: $bg-color;
  color: white;
  padding: 0.5rem 1rem;
  border: none;
  border-radius: $border-radius;
  cursor: pointer;

  &:hover {
    background-color: darken($bg-color, 10%);
  }
}

.btn-primary {
  @include button-style($primary-color);
}

Configuration

Register the Sass loader and pass options on the sass key. The Sass options are passed directly to the Dart Sass / Embedded Sass compiler. Select the implementation with implementation ("sass" or "sass-embedded").

import { defineConfig } from '@visulima/packem/config'
import transformer from '@visulima/packem/transformer/esbuild'
import sassLoader from '@visulima/packem/css/loader/sass'

export default defineConfig({
  transformer,
  rollup: {
    css: {
      mode: 'extract',
      loaders: [sassLoader],
      sass: {
        implementation: 'sass', // or 'sass-embedded'
        style: 'compressed',
        loadPaths: ['node_modules', 'src/styles'],
      },
    },
  },
})

Less

Less preprocessing with full feature support.

Installation

npm install --save-dev less

Usage

// src/styles.less
@primary-color: #007bff;
@border-radius: 0.25rem;

.button-style(@bg-color) {
  background-color: @bg-color;
  color: white;
  padding: 0.5rem 1rem;
  border: none;
  border-radius: @border-radius;
  cursor: pointer;

  &:hover {
    background-color: darken(@bg-color, 10%);
  }
}

.btn-primary {
  .button-style(@primary-color);
}

Configuration

Register the Less loader and pass Less.js compiler options directly on the less key.

import { defineConfig } from '@visulima/packem/config'
import transformer from '@visulima/packem/transformer/esbuild'
import lessLoader from '@visulima/packem/css/loader/less'

export default defineConfig({
  transformer,
  rollup: {
    css: {
      mode: 'extract',
      loaders: [lessLoader],
      less: {
        math: 'parens-division',
        paths: ['node_modules', 'src/styles'],
      },
    },
  },
})

Stylus

Stylus preprocessing with expressive syntax.

Installation

npm install --save-dev stylus

Usage

// src/styles.styl
primary-color = #007bff
border-radius = 0.25rem

button-style(bg-color)
  background-color bg-color
  color white
  padding 0.5rem 1rem
  border none
  border-radius border-radius
  cursor pointer

  &:hover
    background-color darken(bg-color, 10%)

.btn-primary
  button-style(primary-color)

Configuration

Register the Stylus loader and pass Stylus render options directly on the stylus key.

import { defineConfig } from '@visulima/packem/config'
import transformer from '@visulima/packem/transformer/esbuild'
import stylusLoader from '@visulima/packem/css/loader/stylus'

export default defineConfig({
  transformer,
  rollup: {
    css: {
      mode: 'extract',
      loaders: [stylusLoader],
      stylus: {
        compress: true,
        paths: ['node_modules', 'src/styles'],
        use: ['nib'],
      },
    },
  },
})

PostCSS

PostCSS processing with plugin ecosystem support.

Installation

npm install --save-dev postcss postcss-load-config

Configuration

Create a postcss.config.js file:

// postcss.config.js
module.exports = {
  plugins: [
    require('autoprefixer'),
    require('postcss-custom-properties'),
    require('cssnano')({
      preset: 'default'
    })
  ]
}

Or configure directly in Packem on the postcss key:

import { defineConfig } from '@visulima/packem/config'
import transformer from '@visulima/packem/transformer/esbuild'
import postcssLoader from '@visulima/packem/css/loader/postcss'

export default defineConfig({
  transformer,
  rollup: {
    css: {
      mode: 'extract',
      loaders: [postcssLoader],
      postcss: {
        plugins: [
          'autoprefixer',
          ['postcss-custom-properties', { preserve: false }],
          ['cssnano', { preset: 'default' }]
        ]
      }
    }
  }
})
  • autoprefixer - Add vendor prefixes automatically
  • postcss-custom-properties - CSS custom properties support
  • postcss-nested - Nested CSS rules
  • postcss-import - Inline @import rules
  • tailwindcss - Utility-first CSS framework

CSS Modules

CSS Modules provide locally scoped CSS to avoid naming conflicts. Enable them with the autoModules option, which applies to files named [name].module.[ext] (for example Button.module.css).

Enable CSS Modules

import { defineConfig } from '@visulima/packem/config'
import transformer from '@visulima/packem/transformer/esbuild'
import postcssLoader from '@visulima/packem/css/loader/postcss'

export default defineConfig({
  transformer,
  rollup: {
    css: {
      mode: 'extract',
      loaders: [postcssLoader],
      autoModules: true,
    },
  },
})

To restrict CSS Modules to a custom pattern, pass a regular expression to autoModules. Module name generation is customized through postcss.modules:

export default defineConfig({
  transformer,
  rollup: {
    css: {
      mode: 'extract',
      loaders: [postcssLoader],
      autoModules: /\.module\./,
      postcss: {
        modules: {
          generateScopedName: '[name]__[local]___[hash:base64:5]',
          exportGlobals: true,
        },
      },
    },
  },
})

You can also enable modules through postcss.modules directly (set it to true or an options object) instead of using autoModules.

Usage

/* src/Button.module.css */
.button {
  padding: 0.5rem 1rem;
  border: none;
  border-radius: 0.25rem;
  cursor: pointer;
}

.primary {
  background-color: #007bff;
  color: white;
}

.secondary {
  background-color: #6c757d;
  color: white;
}
// src/Button.ts
import styles from './Button.module.css'

export function createButton(variant: 'primary' | 'secondary' = 'primary') {
  const button = document.createElement('button')
  button.className = `${styles.button} ${styles[variant]}`
  button.textContent = 'Click me'
  return button
}

CSS Modules with TypeScript

Set the dts option to generate companion .d.ts files alongside your CSS Modules, giving you IntelliSense and compile-time checks for class names.

css: {
  mode: 'extract',
  loaders: [postcssLoader],
  autoModules: true,
  dts: true,
}

This emits a declaration file next to each module:

// src/Button.module.css.d.ts (auto-generated)
declare const button: string
declare const primary: string
declare const secondary: string

interface ModulesExports {
  button: string
  primary: string
  secondary: string
}

declare const styles: ModulesExports
export default styles
export { button, primary, secondary }

Named Exports

Enable namedExports to export each class as a named binding alongside the default export:

css: {
  loaders: [postcssLoader],
  autoModules: true,
  namedExports: true,
}

Lightning CSS

Lightning CSS provides fast CSS parsing, transformation, and minification, written in Rust. It can be used as a loader, and as a minifier (see Minification).

Installation

npm install --save-dev lightningcss

Configuration

Register the Lightning CSS loader and pass Lightning CSS transform options on the lightningcss key. Browser targets are derived from your project's browserslist configuration.

import { defineConfig } from '@visulima/packem/config'
import transformer from '@visulima/packem/transformer/esbuild'
import lightningcssLoader from '@visulima/packem/css/loader/lightningcss'

export default defineConfig({
  transformer,
  rollup: {
    css: {
      mode: 'extract',
      loaders: [lightningcssLoader],
      lightningcss: {
        drafts: {
          customMedia: true,
        },
      },
    },
  },
})

The targets, minify, code, filename, and cssModules Lightning CSS options are managed by Packem internally and are not configurable through the lightningcss key. Browser targets come from your browserslist configuration.

Features

  • Fast parsing - Written in Rust for maximum performance
  • Modern CSS - Support for CSS nesting, custom media queries, etc.
  • Browser targets - Automatic feature detection based on your browserslist
  • Minification - Built-in CSS minification when used as a minifier

Minification

CSS minification is enabled by passing a minifier object to the minifier option. Packem ships two built-in minifiers as importable sub-paths:

  • @visulima/packem/css/minifier/cssnano — cssnano (requires cssnano installed)
  • @visulima/packem/css/minifier/lightningcss — Lightning CSS (requires lightningcss installed)

There is no boolean minify / minimize option for CSS. Minification is opt-in by assigning a minifier object to minifier.

cssnano

import { defineConfig } from '@visulima/packem/config'
import transformer from '@visulima/packem/transformer/esbuild'
import postcssLoader from '@visulima/packem/css/loader/postcss'
import cssnanoMinifier from '@visulima/packem/css/minifier/cssnano'

export default defineConfig({
  transformer,
  rollup: {
    css: {
      mode: 'extract',
      loaders: [postcssLoader],
      minifier: cssnanoMinifier,
      cssnano: {
        preset: ['default', {
          discardComments: { removeAll: true },
        }],
      },
    },
  },
})

Lightning CSS

import { defineConfig } from '@visulima/packem/config'
import transformer from '@visulima/packem/transformer/esbuild'
import postcssLoader from '@visulima/packem/css/loader/postcss'
import lightningcssMinifier from '@visulima/packem/css/minifier/lightningcss'

export default defineConfig({
  transformer,
  rollup: {
    css: {
      mode: 'extract',
      loaders: [postcssLoader],
      minifier: lightningcssMinifier,
    },
  },
})

Output Modes

The mode option controls how CSS reaches the bundle. The default is "inject".

Extract

Writes CSS to a separate .css file next to the generated JS.

export default defineConfig({
  transformer,
  rollup: {
    css: {
      mode: 'extract',
      loaders: [postcssLoader],
    },
  },
})

To extract to a fixed file name (relative to the output directory), use the tuple form:

css: {
  mode: ['extract', 'styles.css'],
  loaders: [postcssLoader],
}

Inject (default)

Embeds the CSS inside the JavaScript bundle and injects it into <head> at runtime. This requires @visulima/css-style-inject to be installed.

export default defineConfig({
  transformer,
  rollup: {
    css: {
      mode: 'inject',
      loaders: [postcssLoader],
    },
  },
})
npm install --save-dev @visulima/css-style-inject

You can also pass injector options or your own injector function with the tuple form ['inject', options | fn].

Inline

Embeds the processed CSS as a string export in the JavaScript module without runtime injection.

css: {
  mode: 'inline',
  loaders: [postcssLoader],
}

Emit

Emits pure processed CSS and passes it along the build pipeline. Useful for preprocessing CSS before it is consumed by other plugins.

css: {
  mode: 'emit',
  loaders: [postcssLoader],
}

Runtime Injection

When using inject mode, Packem relies on @visulima/css-style-inject. You can also call it directly for dynamic, runtime-generated styles:

import { injectStyles } from '@visulima/css-style-inject'

const css = `
  .dynamic-styles {
    color: red;
    font-weight: bold;
  }
`

injectStyles(css, {
  id: 'dynamic-styles',
  prepend: false
})

Source Maps

Enable CSS source maps with the sourceMap option:

css: {
  mode: 'extract',
  loaders: [postcssLoader],
  sourceMap: true, // or 'inline'
}

You can also pass a tuple with SourceMapOptions, e.g. sourceMap: [true, { /* options */ }].

Advanced Features

CSS Custom Properties

:root {
  --primary-color: #007bff;
  --secondary-color: #6c757d;
  --border-radius: 0.25rem;
}

.btn {
  background-color: var(--primary-color);
  border-radius: var(--border-radius);
}

CSS Nesting (with PostCSS or Lightning CSS)

.card {
  padding: 1rem;
  border: 1px solid #dee2e6;

  .title {
    font-size: 1.25rem;
    font-weight: bold;
    margin-bottom: 0.5rem;
  }

  .content {
    color: #6c757d;

    p {
      margin-bottom: 1rem;

      &:last-child {
        margin-bottom: 0;
      }
    }
  }
}

Container Queries

.card {
  container-type: inline-size;
}

@container (min-width: 400px) {
  .card-content {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 1rem;
  }
}

Framework Integration

React

// React with CSS Modules
import styles from './Button.module.css'

interface ButtonProps {
  variant?: 'primary' | 'secondary'
  children: React.ReactNode
}

export function Button({ variant = 'primary', children }: ButtonProps) {
  return (
    <button className={`${styles.button} ${styles[variant]}`}>
      {children}
    </button>
  )
}

Vue

<template>
  <button :class="[styles.button, styles[variant]]">
    <slot />
  </button>
</template>

<script setup lang="ts">
import styles from './Button.module.css'

interface Props {
  variant?: 'primary' | 'secondary'
}

withDefaults(defineProps<Props>(), {
  variant: 'primary'
})
</script>

Troubleshooting

CSS Not Loading

  1. Check imports - Ensure CSS files are imported in your JavaScript/TypeScript
  2. Register a loader - Add the relevant loader (e.g. postcssLoader) to the loaders array
  3. Verify file paths - Check that CSS file paths are correct

Preprocessor Errors

  1. Install dependencies - Make sure preprocessor packages are installed (sass, less, stylus)
  2. Register the loader - Add the matching loader to css.loaders
  3. Check syntax - Verify preprocessor syntax is correct

CSS Modules Not Working

  1. Enable CSS Modules - Set css.autoModules: true (or configure css.postcss.modules)
  2. File naming - Use the .module.css suffix for CSS Modules files
  3. Import syntax - Use import styles from './file.module.css'

Performance Issues

  1. Enable minification - Assign a minifier to css.minifier
  2. Extract CSS - Use css.mode: 'extract' for better caching
  3. Optimize imports - Remove unused CSS imports
  4. Use Lightning CSS - Switch to the Lightning CSS loader/minifier for better performance

Use packem build --verbose to see detailed CSS processing information and identify issues.

Examples

Check out our CSS examples:


Need more help? Check the CSS troubleshooting guide or explore CSS examples.

Support

Contribute to our work and keep us going

Community is the heart of open source. The success of our packages wouldn't be possible without the incredible contributions of users, testers, and developers who collaborate with us every day.Want to get involved? Here are some tips on how you can make a meaningful impact on our open source projects.

Ready to help us out?

Be sure to check out the package's contribution guidelines first. They'll walk you through the process on how to properly submit an issue or pull request to our repositories.

Submit a pull request

Found something to improve? Fork the repo, make your changes, and open a PR. We review every contribution and provide feedback to help you get merged.

Good first issues

Simple issues suited for people new to open source development, and often a good place to start working on a package.
View good first issues