symfonycasts/sass-bundle
A Symfony bundle that compiles Sass/SCSS to CSS using Dart Sass and integrates with AssetMapper. It provides build and watch commands, source maps, and simple configuration for modern asset pipelines.
composer require symfonycasts/sass-bundle — the Flex recipe registers the bundle and drops a config/packages/sass.yaml. If you're not using Flex, add Symfonycasts\SassBundle\SymfonycastsSassBundle to config/bundles.php.config/packages/sass.yaml first. That file defines the two paths that matter: where your .scss entries live (sass_path, typically assets/styles) and where compiled .css lands (css_path, also under assets/ so AssetMapper can serve it). Everything else in the bundle is downstream of those two knobs.assets/styles/app.scss with a few rules (or @use some partials) is your first real use case.php bin/console sass:build. Confirm a sibling .css file appears in css_path.assets/, AssetMapper already knows about it. Either <link rel="stylesheet" href="{{ asset('styles/app.css') }}"> in base.html.twig, or import './styles/app.css' from your app.js entrypoint if you prefer bundling through importmap.php bin/console sass:watch in a second terminal next to symfony serve. Edit a partial, refresh the browser, see the change. That's the 90% loop.First pitfall to know before you start: the very first build downloads the Dart Sass binary for your platform. If you're offline, behind a proxy, or in a locked-down CI, the build will hang or fail before any SCSS is even parsed — see Gotchas.
Entrypoints vs. partials. Dart Sass compiles every non-partial .scss it finds under sass_path; anything starting with _ is treated as a partial and skipped as an output. So the conventional layout is one thin entry per page/section and a shared library:
assets/styles/
app.scss -> app.css
admin.scss -> admin.css
_variables.scss
_mixins.scss
components/_buttons.scss
pages/_dashboard.scss
app.scss is just @use 'variables'; @use 'components/buttons'; .... Keep entry files declarative; put real rules in partials.
Module system. Use @use/@forward (not @import). Give each partial a namespace and forward a shared API from an _index.scss so callers do @use 'components' as c; instead of reaching into deep paths. This is also what protects you from the Dart Sass @import deprecation warnings.
Dev workflow. Run two watchers in parallel: sass:watch for SCSS→CSS and asset-map:compile --watch (or just symfony serve) for AssetMapper's importmap rebuilds. A composer dev script using concurrently or a Procfile-style setup pays for itself immediately:
"scripts": {
"dev": ["php bin/console sass:watch", "symfony serve"]
}
Deploy pipeline. The build order matters: sass:build must run before asset-map:compile, because the latter fingerprints whatever CSS exists at that moment. In a Dockerfile, that's typically composer install --no-dev, sass:build, asset-map:compile, all during image build. Add sass:build to a post-install-cmd only if you're sure the binary is available at install time.
Per-page CSS. Since each entry compiles independently, you can emit a small app.css plus admin.css, checkout.css, etc., and include only the ones a given template needs. This is a legitimate reason to use this bundle instead of a single bundled stylesheet — take advantage of it.
Programmatic use. The bundle exposes the compiler as a service (a SassBuilder-style object). If you need compilation triggered from a custom command, a test bootstrap, or a deploy script that isn't a shell call, inject it rather than shelling out to sass:build.
Where you shouldn't fight it. This bundle is intentionally a thin wrapper around the Sass CLI scoped to AssetMapper. If you need PostCSS, autoprefixing, CSS modules, or a full frontend toolchain, that's Webpack Encore / Vite territory. Don't try to bolt a plugin pipeline onto this.
sass:build fetches Dart Sass and stores it under var/. Consequences: it needs network access and a writable var/; cache:clear may remove it and force a re-download; CI images that build offline need the binary pre-provisioned and pointed to explicitly via the bundle's binary path config, or a sass executable on PATH.sass_path vs css_path mismatch with AssetMapper. If you point css_path outside the directories AssetMapper scans, the CSS compiles fine but {{ asset(...) }} 404s or asset-map:compile never sees it. Either keep output under assets/ (the default and the happy path) or register the extra directory in asset_mapper.paths.assets/ is an artifact. Gitignore it and make sure every clone/CI runs sass:build — otherwise a fresh checkout has a broken importmap the first time someone runs asset-map:compile. Conversely, don't gitignore and skip the build step in your deploy.sass.yaml changes are not hot-reloaded — restart sass:watch after touching paths or output style. Also restart after composer require of anything that affects the binary.sass:watch with a polling flag if the bundle exposes one (check sass:watch --help) or fall back to re-running sass:build on save via a small file-watcher script.@import deprecation noise. Recent Dart Sass versions emit deprecation warnings for @import, and the bundle passes them straight through. Migrate to @use, or, if the config surface allows, silence specific deprecations; long-term, don't pin an old binary just to hide warnings — you'll hit the same wall later.sass.yaml — compressed + no maps for prod, expanded + maps for dev..exe/.bat shim is pointed at correctly. "Permission denied" or "not a valid Win32 application" almost always means a misconfigured binary path.sass:watch has written the new CSS, so you see the old styles on HMR/live-reload. Either add a small debounce to your livereload setup or compile on save via an editor task and let asset-map:compile --watch pick it up.--style=compressed, --no-source-map, --load-path=...) without forking the bundle. That's the safest way to add behavior the config doesn't expose yet.How can I help you explore Laravel packages today?