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 withSourceMapOptions(@default false).dts— generate.d.tsfiles 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 sassnpm install --save-dev sass-embeddedUsage
// 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 lessUsage
// 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 stylusUsage
// 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-configConfiguration
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' }]
]
}
}
}
})Popular PostCSS Plugins
- 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 lightningcssConfiguration
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 (requirescssnanoinstalled)@visulima/packem/css/minifier/lightningcss— Lightning CSS (requireslightningcssinstalled)
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-injectYou 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
- Check imports - Ensure CSS files are imported in your JavaScript/TypeScript
- Register a loader - Add the relevant loader (e.g.
postcssLoader) to theloadersarray - Verify file paths - Check that CSS file paths are correct
Preprocessor Errors
- Install dependencies - Make sure preprocessor packages are installed (
sass,less,stylus) - Register the loader - Add the matching loader to
css.loaders - Check syntax - Verify preprocessor syntax is correct
CSS Modules Not Working
- Enable CSS Modules - Set
css.autoModules: true(or configurecss.postcss.modules) - File naming - Use the
.module.csssuffix for CSS Modules files - Import syntax - Use
import styles from './file.module.css'
Performance Issues
- Enable minification - Assign a minifier to
css.minifier - Extract CSS - Use
css.mode: 'extract'for better caching - Optimize imports - Remove unused CSS imports
- 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:
- Basic CSS Processing
- Sass/SCSS Integration
- CSS Modules
- PostCSS Configuration
- Lightning CSS
- React with CSS Modules
Need more help? Check the CSS troubleshooting guide or explore CSS examples.