CssModuleBundle is a Symfony bundle that enables the use of CSS Modules and JavaScript module imports directly within Twig templates.
/* button.module.scss */
.button {
background-color: blue;
color: white;
padding: 10px 20px;
border-radius: 5px;
text-decoration: none;
}{# Import the CSS module defined above #}
{% importModule 'button.module.scss' %}
{# Use a scoped class from the imported module by string #}
<a class="{{ scope('button') }}">Click me</a>
{# ...or by array #}
<a class="{{ scope(['button', 'button2']) }}">Click me</a>CSS Modules bring scoped class names to CSS, avoiding naming collisions and promoting modular architecture. This bundle brings the same benefits to Symfony + Twig.
If you're familiar with CSS Modules, particularly in React, you'll appreciate the ability to:
- Use short, unique class names without global conflicts.
- Keep styles encapsulated at the component level.
- Apply the same development patterns in Symfony projects with Twig.
composer require mak/css-module-bundleIn your webpack.config.js:
Encore
// ...
.addEntry('app', [
'./assets/app.js',
...glob.sync(["./templates/**/*.html.twig"]),
])
.addLoader({
test: /\.twig$/,
use: [
{
loader: path.resolve(
__dirname,
"vendor/mak/css-module-bundle/Resources/webpack/TwigLoader.js"
),
},
],
})
.configureCssLoader((options) => {
options.modules = {
auto: (resourcePath) => /\.module\.\w+$/i.test(resourcePath),
localIdentName: "[hash:base64:5]",
};
})# config/packages/mak_css_module.yaml
mak_css_module:
localIdentName: '[hash:base64]'
localIdentContext: '%kernel.project_dir%'
localIdentHashSalt: null{# Import CSS module into the current template context #}
{% importModule 'button.module.scss' %}
{# Apply a scoped class from the imported module #}
<a class="{{ scope('button') }}">Click me</a>
{# Alternatively, specify the module explicitly #}
<a class="{{ scope('button', 'button.module.scss') }}">Click me</a>
β οΈ Note:
Imported modules only apply to the current template to prevent unintended side effects in included templates.
Inspired by Atomic Design and modern component-based development, you can colocate templates, styles, and JavaScript files:
templates/
ββ components/
β ββ atoms/
β β ββ sample-atom/
β β β ββ sample-atom.html.twig
β β β ββ sample-atom.module.css
β β β ββ sample_atom_controller.js
β ββ molecules/
β ββ organisms/
ββ pages/
ββ base/
With importModule, CSS and JS are automatically bundled for each component.
To enable autoloading of Stimulus controllers in your templates:
// assets/bootstrap.js
app.load(
definitionsFromContext(
require.context(
"@symfony/stimulus-bridge/lazy-controller-loader!../templates",
true,
/\.[jt]sx?$/
)
)
);This allows Webpack to bundle Stimulus controllers alongside your Twig templates.
- The bundle mimics Webpackβs
css-loaderfunctionality to hash class names. importModuleregisters the module within a template.scope()computes the hashed class name at compile time.- Included templates are isolated to prevent shared scope.
β οΈ Note:
scope()accepts raw string input for class names. Input validation is out of scope.
These tools remove unused CSS by scanning for class names. Since CSS module hashes aren't explicitly written in Twig/JS, you must:
- Add a prefix to all CSS module hashes:
mak_css_module:
localIdentName: '_[hash:base64]'-
Update your Encore config similarly.
-
Safelist the prefix in PurgeCSS:
new PurgeCSSPlugin({
paths: glob.sync([
`${PATHS.templates}/**/*.html.twig`,
`${PATHS.assets}/**/*.{js,ts}`,
], { nodir: true }),
safelist: {
deep: [/^_/],
},
extractors: [
{
extractor: purgeHtml,
extensions: ["html", "twig"],
},
],
})No. All hashing is done at Twig compile time, so runtime performance is unaffected. In fact, shorter hashed class names may slightly improve render efficiency.
No additional setup is needed. Just use .module.scss or .module.less as usual. Webpack will handle them
according to your preprocessor loader configuration.
Yes, using sass-resources-loader:
Encore
.configureLoaderRule("scss", (loaderRule) => {
loaderRule.oneOf.forEach((rule) => {
rule.use.push({
loader: "sass-resources-loader",
options: {
resources: [
path.resolve(__dirname, "./node_modules/bootstrap5/scss/_mixins.scss"),
path.resolve(__dirname, "./assets/scss/global.scss"),
],
},
});
});
});This will inject shared mixins and variables into every .module.scss file.
Pull requests are welcome! If you find bugs or have feature suggestions, feel free to open an issue or tweet about it using #css-modules-bundle.