This guide covers migrating the CSS Modules portion of an Ember addon from v1 (with ember-css-modules) to v2 format (with glimmer-local-class-transform and rollup-plugin-preprocess-css-modules).
The migration can be done in stages. Some preparation steps can be done while the addon is still in v1 format, reducing the amount of change needed in the final conversion.
You should be familiar with the v2 addon format generally. This guide covers only the CSS Modules portion of the migration. For the broader v1-to-v2 addon migration, see the Embroider v2 addon guide.
These steps work with ember-css-modules while the addon is still in v1 format. Each produces a working addon.
If you have components using pod layout (component/styles.css) or classic layout (styles/components/foo.css), move them to colocated files alongside the component's template/JS. ember-css-modules already supports colocated layout:
Pod layout → colocated:
addon/components/my-component/styles.css → addon/components/my-component.css
Classic layout → colocated:
addon/styles/components/my-component.css → addon/components/my-component.css
This step can be done one component at a time.
ember-css-modules supports configuring the file extension via the addon's index.js. You can rename your CSS files to .module.css now — matching the convention the v2 build will expect — by setting the extension option:
// index.js
module.exports = {
name: require('./package').name,
options: {
cssModules: {
extension: 'module.css',
},
},
};Then rename your files:
addon/components/my-component.css → addon/components/my-component.module.css
When you're ready to convert to the v2 addon format, follow these steps. The broader structural changes (adding Rollup, addon/ → src/, new package.json exports) are part of the general v2 addon migration — this guide covers the CSS Modules–specific parts.
Remove ember-css-modules from dependencies. Add the new packages:
npm uninstall ember-css-modules
npm install --save glimmer-local-class-transform
npm install --save-dev rollup-plugin-preprocess-css-modulesglimmer-local-class-transform goes in dependencies because consuming apps need it at build time. rollup-plugin-preprocess-css-modules is only used to build the addon, so it goes in devDependencies.
This Babel config is used when building the addon for publication. It must include glimmer-local-class-transform in the template compilation transforms, with targetFormat: 'hbs' so that templates are compiled to .hbs (which Rollup and the consuming app can process):
// babel.publish.config.cjs
module.exports = {
plugins: [
'@embroider/addon-dev/template-colocation-plugin',
[
'babel-plugin-ember-template-compilation',
{
targetFormat: 'hbs',
transforms: ['glimmer-local-class-transform'],
},
],
[
'module:decorator-transforms',
{
runtime: {
import: 'decorator-transforms/runtime-esm',
},
},
],
],
generatorOpts: {
compact: false,
},
};Add preprocessCSSModules() to your Rollup plugins, include .hbs in the Babel extensions, and use addon.keepAssets(['**/*.css']) to preserve the CSS output:
// rollup.config.mjs
import { babel } from '@rollup/plugin-babel';
import { Addon } from '@embroider/addon-dev/rollup';
import { preprocessCSSModules } from 'rollup-plugin-preprocess-css-modules';
const addon = new Addon({
srcDir: 'src',
destDir: 'dist',
});
export default {
output: addon.output(),
plugins: [
addon.publicEntrypoints(['**/*.js', 'index.js']),
addon.appReexports([
'components/**/*.js',
'helpers/**/*.js',
'modifiers/**/*.js',
'services/**/*.js',
]),
addon.dependencies(),
babel({
extensions: ['.hbs', '.js', '.gjs'],
babelHelpers: 'bundled',
configFile: './babel.publish.config.cjs',
}),
// Ensure that standalone .hbs files are properly integrated as Javascript.
addon.hbs(),
// Ensure that .gjs files are properly integrated as Javascript.
addon.gjs(),
// Preprocess CSS Modules so consumers get plain CSS.
preprocessCSSModules(),
// Keep CSS files in the published output.
addon.keepAssets(['**/*.css']),
addon.clean(),
],
};Move your component files from addon/ to src/ (standard v2 addon layout). If you've already renamed to .module.css during preparation, just move the files:
addon/components/my-component.hbs → src/components/my-component.hbs
addon/components/my-component.js → src/components/my-component.js
addon/components/my-component.module.css → src/components/my-component.module.css
If you haven't renamed yet, rename as you move:
addon/components/my-component.css → src/components/my-component.module.css
You can also remove addon/styles/.placeholder — it was only needed by ember-css-modules to trigger CSS processing in the classic build.
Remove the app/ re-export files (e.g. app/components/my-component.js) — v2 addons handle re-exports via addon.appReexports() in the Rollup config.
No changes needed. local-class works identically:
In a v2 addon, CSS Modules are an implementation detail — consuming apps shouldn't need to know or care that your addon uses them internally.
rollup-plugin-preprocess-css-modules handles this by preprocessing .module.css imports at build time. It:
- Processes each
.module.cssfile through CSS Modules, generating scoped class names. - Replaces the JavaScript
import styles from './my-component.module.css'(whichglimmer-local-class-transformgenerates) with a static object mapping original class names to their scoped equivalents. - Outputs a plain
.cssfile with the scoped class names already applied.
The result is that your published addon contains only standard CSS and JS — no CSS Modules runtime is needed by consumers.
See the rollup-plugin-preprocess-css-modules README for configuration options like generateScopedName and getOutputFilename.