Setting Up Tailwind CSS with Symfony AssetMapper: The Modern Setup Without Node.js
AI generated
</>
tw
Tailwind CSS · Symfony · AssetMapper · Standalone CLI · PHP
Tailwind CSS with Symfony AssetMapper:
The Modern Setup Without Node.js

Symfony 7 makes Webpack Encore optional: AssetMapper manages JavaScript imports natively through importmap, and the Tailwind CSS Standalone CLI processes CSS without npm. The result is a modern frontend setup that needs no Node.js in the production build, simpler to deploy and simpler to maintain.

14 min read AssetMapper · Standalone CLI · importmap · Twig · Deployment Symfony 7 · PHP 8.3+ · Tailwind CSS v3 · v4

1. Why AssetMapper Instead of Webpack Encore?

Symfony Webpack Encore was long the standard solution for asset management in Symfony projects: JavaScript bundles, CSS preprocessing, Babel transpilation. For many projects that brought considerable overhead: a node_modules instance with thousands of packages, a complex Webpack configuration, and a long build time that slows down every deployment process. With Symfony 6.3 and the Tailwind CSS Symfony AssetMapper, there is an alternative that fits many projects far better: AssetMapper treats JavaScript files as ES modules that the browser can load natively, and importmap maps package paths to CDN URLs or local files.

AssetMapper is built on a simple idea: modern browsers support ES modules natively. It is no longer necessary to bundle JavaScript for it to run in the browser. AssetMapper copies asset files with a content-hash fingerprint into a public directory and manages the importmap automatically. For CSS, AssetMapper is not a full replacement for PostCSS, which is where the Tailwind CSS Symfony combination with the Standalone CLI comes in: Tailwind CSS processes CSS without npm, using only a single binary that requires no Node.js installation.

2. Symfony AssetMapper: Fundamentals and Concepts

Symfony AssetMapper is installed as a bundle via composer require symfony/asset-mapper. After installation there are two new configuration files: config/packages/asset_mapper.yaml defines the directories that AssetMapper watches, and importmap.php holds the importmap configuration, which JavaScript packages are available and where they come from. The command php bin/console importmap:require stimulus downloads Stimulus as a package and registers it in importmap.php automatically.

The assets/ directory at the project root is the default entry point for source files. AssetMapper watches this directory and produces versioned copies in public/assets/ at build time or on the first request. The filenames contain a content hash, app.1a2b3c.css, which changes whenever the file content changes. This gives you automatic cache busting without any manual version management. For the Tailwind CSS Symfony AssetMapper setup, the interplay is important: Tailwind CSS writes its output file into the assets/ directory, AssetMapper versions it, and it gets wired into Twig templates from there.

3. Tailwind CSS Standalone CLI: Setup and Configuration

The Tailwind CSS Standalone CLI is a precompiled binary that processes Tailwind CSS without Node.js and without npm. It is available for Linux (x64, arm64), macOS (x64, arm64), and Windows, and bundles all necessary dependencies as a single-file binary. For the Tailwind CSS Symfony setup, that is the decisive advantage: the build server does not need Node.js installed. The binary is downloaded once, checked into the project or provided through a separate download step, and then used for all CSS builds.

Installation in the Symfony project: the binary is downloaded into the project directory (typically bin/tailwind) and made executable. The input CSS lives in assets/styles/app.css and contains the Tailwind directives. The output is written into the same directory or a subdirectory so that AssetMapper can pick it up. One important aspect of the Tailwind CSS Symfony AssetMapper setup: the output file must live inside the assets/ directory so that AssetMapper versions it. Alternatively, a separate output directory can be configured, but that requires additional AssetMapper path configuration.


/* assets/styles/app.css - Main CSS entry point for Tailwind CSS + Symfony */

/* Tailwind base reset and preflight */
@tailwind base;

/* Tailwind component classes (if used) */
@tailwind components;

/* All utility classes - generated from Twig, PHP and JS content scan */
@tailwind utilities;

/* Custom base styles after Tailwind */
@layer base {
  :root {
    --color-brand: #0ea5e9;
    --color-brand-dark: #0284c7;
  }

  html {
    @apply scroll-smooth;
  }

  body {
    @apply font-sans text-slate-800 antialiased;
  }
}

/* Custom component classes that are too complex for utilities alone */
@layer components {
  .btn-primary {
    @apply bg-sky-600 text-white font-semibold px-5 py-2.5 rounded-xl
           hover:bg-sky-700 focus:outline-none focus:ring-2 focus:ring-sky-500 focus:ring-offset-2
           transition-colors duration-150 disabled:opacity-50 disabled:cursor-not-allowed;
  }

  .card {
    @apply bg-white border border-slate-200 rounded-2xl p-6 shadow-sm;
  }
}

4. tailwind.config.js for Symfony Projects

The tailwind.config.js for a Symfony project must cover every location where Tailwind classes can appear: Twig templates, PHP controllers (when classes are generated as PHP strings), JavaScript files in the assets/ directory, and MDX or other documentation files. The content configuration must include all of these paths so that the purge mechanism does not remove classes that are actually needed.

Especially important in the Tailwind CSS Symfony context: Twig templates that assemble Tailwind classes from PHP variables or Twig variables are a common pitfall. If a Twig template renders class="bg-{{ category.color }}-500", then bg-{{ category.color }}-500 is not a complete class string for Tailwind's scanner. The same rule applies here as in any other Tailwind project: complete class strings in a lookup file or in the safelist. The Symfony configuration structure offers a natural solution: a PHP file config/tailwind-classes.php that holds an array with all complete classes and is registered in Tailwind's content path.


// tailwind.config.js - Symfony + AssetMapper project configuration
/** @type {import('tailwindcss').Config} */
module.exports = {
  // Scan all locations where Tailwind classes might appear
  content: [
    './assets/**/*.js',
    './assets/**/*.ts',
    './templates/**/*.html.twig',
    './templates/**/*.twig',
    // PHP files that generate class strings
    './src/**/*.php',
    // Config file with complete class strings for dynamic classes
    './config/tailwind-classes.php',
  ],

  // Class-based dark mode (toggle with JS/Stimulus)
  darkMode: 'class',

  theme: {
    extend: {
      fontFamily: {
        // Use system font stack - no custom fonts loaded
        sans: ['ui-sans-serif', 'system-ui', 'sans-serif'],
      },
      colors: {
        brand: {
          50:  '#f0f9ff',
          100: '#e0f2fe',
          500: '#0ea5e9',
          600: '#0284c7',
          700: '#0369a1',
        },
      },
    },
  },

  safelist: [
    // Dynamic status badge colors from database
    { pattern: /^(bg|text|border)-(red|yellow|green|blue)-(100|200|500|600|700)$/ },
  ],

  plugins: [
    // Prose plugin for rich text content from CMS
    require('@tailwindcss/typography'),
  ],
};

5. Twig Integration: Wiring Up CSS and importmap

In the base Twig template, the Tailwind CSS output file is wired in through the AssetMapper function asset(). AssetMapper automatically replaces the path to the source file with the versioned path that carries the content hash. In addition, AssetMapper renders the importmap as an inline script tag in the <head>, which is needed so the browser can resolve the import statements inside JavaScript files. Both happen through Twig functions that the bundle provides.

An important aspect of the Tailwind CSS Symfony AssetMapper setup: the CSS file must be included before the importmap tag so that no FOUC (Flash of Unstyled Content) occurs on first paint. The base template should also contain an empty {% block stylesheets %} and {% block javascripts %} so that child templates can add page-specific CSS or JavaScript. AssetMapper provides the importmap Twig helper for this, which renders all importmap entries as a JSON script tag and stays automatically up to date whenever new packages are added via php bin/console importmap:require.


{# templates/base.html.twig - Symfony base template with AssetMapper + Tailwind CSS #}
<!DOCTYPE html>
<html lang="en" class="">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>{% block title %}Mironsoft{% endblock %}</title>

  {# Link to Tailwind CSS output - AssetMapper adds content hash automatically #}
  <link rel="stylesheet" href="{{ asset('styles/app.css') }}">

  {% block stylesheets %}{% endblock %}

  {# importmap - maps bare module specifiers to URLs for native ES module imports #}
  {{ importmap('app') }}
</head>
<body>
  {% block body %}{% endblock %}

  {% block javascripts %}{% endblock %}
</body>
</html>

6. Watch Mode and Production Build

The Tailwind CSS Symfony development workflow consists of two parallel processes: the Symfony development server (symfony serve or php -S localhost:8000 -t public/) and the Tailwind CSS watch process. The Standalone CLI starts watch mode with ./bin/tailwind -i assets/styles/app.css -o assets/styles/app.built.css --watch. The --watch flag monitors all source files listed in the content array of the configuration and regenerates the CSS output on every change. The output file lives in the assets/ directory, gets picked up by AssetMapper, and is available immediately after a browser refresh.

For the production build, the Standalone CLI adds the --minify option: ./bin/tailwind -i assets/styles/app.css -o assets/styles/app.built.css --minify. This is followed by the AssetMapper step: php bin/console asset-map:compile copies all assets with a content hash into the public/assets/ directory and writes an assets.json file that holds the mapping from source path to versioned path. This step fully replaces the Webpack build, no Node.js, no npm, no Webpack configuration. The deploy sequence in a Makefile or CI script ends up noticeably shorter than in a classic Encore setup.


# Makefile - Tailwind CSS + Symfony AssetMapper build commands

# Development: watch mode for CSS (run in parallel with symfony serve)
.PHONY: watch
watch:
	./bin/tailwind -i assets/styles/app.css -o assets/styles/app.built.css --watch

# Production: minified CSS + asset compilation
.PHONY: build
build:
	./bin/tailwind -i assets/styles/app.css -o assets/styles/app.built.css --minify
	php bin/console asset-map:compile

# Download Tailwind Standalone CLI for the current platform
.PHONY: install-tailwind
install-tailwind:
	curl -sLO https://github.com/tailwindlabs/tailwindcss/releases/latest/download/tailwindcss-linux-x64
	chmod +x tailwindcss-linux-x64
	mv tailwindcss-linux-x64 bin/tailwind

# Full deploy sequence
.PHONY: deploy
deploy: build
	php bin/console cache:clear --env=prod
	php bin/console cache:warmup --env=prod

7. JavaScript with importmap and Stimulus

Symfony AssetMapper does not just manage JavaScript as static files, it offers a complete solution for JavaScript dependency management via importmap. The command php bin/console importmap:require @hotwired/stimulus downloads Stimulus, registers it in importmap.php, and makes it available under the name @hotwired/stimulus in JavaScript imports. The same applies to Alpine.js (importmap:require alpinejs), Chart.js, or any other JavaScript library available as an ESM module.

Together with Tailwind CSS Symfony AssetMapper, Stimulus is the recommended path for JavaScript interactivity: Stimulus controllers are written as separate JavaScript files in the assets/controllers/ directory, registered by AssetMapper, and loaded automatically through the @hotwired/stimulus-loading package. The naming-convention-based auto-registration makes adding new controllers trivial. Alpine.js is an alternative for simpler interactions directly inside the Twig template, similar to how it is used in Hyva themes for Magento. The choice between Stimulus and Alpine.js depends on the complexity of the interactivity and the preference of the team.

8. Deployment: Asset Fingerprinting and Cache Busting

Deploying a Tailwind CSS Symfony project with AssetMapper is noticeably simpler than a classic Encore deployment. In the production build, asset-map:compile carries out all the necessary steps: it reads all assets from the configured paths, computes content hashes, copies the files with the hash in the filename into public/assets/, and writes a manifest file. Twig then uses the manifest file to resolve {{ asset('styles/app.css') }} to the correct versioned path. That is automatic cache busting without any manual version management.

In a CI/CD workflow with GitHub Actions or GitLab CI, the pipeline is simple: checkout, Composer install, Tailwind binary download (or from the repository), Tailwind build, asset-map:compile, and then the PHP application deploy. Since Node.js is no longer needed, the Node.js installation step in the CI agent disappears entirely. That reduces setup time and makes the pipeline easier to maintain. The Tailwind binary can be checked directly into the Git repository (the binary is roughly 35 MB), which removes the dependency on an external download in the pipeline.

9. AssetMapper vs. Webpack Encore: Direct Comparison

The choice between AssetMapper and Webpack Encore depends on the requirements of the project. For most Symfony projects that do not need complex JavaScript bundles, tree shaking, or module-specific optimizations, AssetMapper is the more modern and simpler choice.

Criterion AssetMapper + Standalone CLI Webpack Encore Recommendation
Node.js needed in the build No (Standalone CLI only) Yes AssetMapper
Configuration overhead Low (one config file) High (webpack.config.js) AssetMapper
JavaScript bundling No bundling (native ESM) Full bundling Encore for complex JS
Browser compatibility Modern browsers (ESM) All (via transpilation) Encore for IE requirements
Cache busting Automatic (content hash) Automatic (manifest) Equivalent

The Tailwind CSS Symfony AssetMapper combination is ideal for projects that primarily use server-side rendering with Twig, use JavaScript for simple interactions (Stimulus, Alpine.js), and do not need legacy browser support. Webpack Encore remains the right choice for single-page applications with React or Vue, for projects that need IE support, or for very complex JavaScript build requirements. For the typical Symfony monolith with primarily server-side rendering, AssetMapper is today the preferred choice of the Symfony core team.

Mironsoft

Symfony development, Tailwind CSS, and modern frontend architectures

Migrating a Symfony project to AssetMapper?

We help you migrate from Webpack Encore to Symfony AssetMapper and integrate Tailwind CSS with the Standalone CLI, cleanly structured, without downtime, and with a fully adapted CI/CD pipeline.

Migration

From Webpack Encore to AssetMapper, migrating JavaScript imports and the CSS pipeline

Tailwind setup

Configuring the Standalone CLI, optimizing tailwind.config.js for Symfony projects

CI/CD adaptation

GitHub Actions or GitLab CI without Node.js, simplifying the build pipeline

10. Summary

The Tailwind CSS Symfony AssetMapper setup is the modern alternative to Webpack Encore for Symfony projects: no Node.js in the production build, no complex Webpack configuration file, no node_modules to manage. The Tailwind CSS Standalone CLI processes CSS from a single binary. AssetMapper takes care of content fingerprinting, cache busting, and importmap generation for JavaScript. Twig wires both in through {{ asset() }} and {{ importmap() }}. The result is a simple, maintainable frontend setup that requires noticeably less configuration in CI/CD pipelines.

The limits of this setup: without Webpack bundling there is no tree shaking for JavaScript libraries and no transpilation for very old browsers. For single-page applications with React or Vue, Webpack Encore or Vite is still the better choice. But for server-rendered PHP with Twig and moderate JavaScript complexity, AssetMapper is clearly the preferred architecture today, following Symfony's philosophy of reducing complexity where it is not needed and relying on native browser features instead.

Tailwind CSS Symfony AssetMapper: The Essentials at a Glance

Standalone CLI

Download the binary, --minify for production, --watch for development. No Node.js, no npm needed.

AssetMapper

asset-map:compile for production, versions all assets with a content hash, automatic cache busting.

Twig integration

{{ asset('styles/app.css') }} and {{ importmap('app') }}, AssetMapper resolves paths to versioned URLs.

content configuration

Include all Twig template paths, PHP files, and JS files in the tailwind.config.js content array, otherwise classes are missing from the build.

11. FAQ: Tailwind CSS Symfony AssetMapper

1What is Symfony AssetMapper?
An alternative to Webpack Encore: asset fingerprinting, importmap for ES modules, no Node.js build step. Included in Symfony from version 6.3 onward.
2What is the Tailwind CSS Standalone CLI?
A precompiled binary without Node.js. --watch for dev, --minify for production. Available for Linux, macOS, and Windows.
3Do I need Node.js?
No. Standalone CLI plus AssetMapper means no Node.js, no npm in the production build. Not needed in the CI agent either.
4Twig integration for Tailwind CSS?
{{ asset('styles/app.css') }}, AssetMapper resolves it to the versioned path. {{ importmap('app') }} renders the importmap for JS imports.
5What is importmap in Symfony?
importmap.php maps package names to JS files. importmap:require alpinejs adds Alpine.js. No CDN link needed.
6Installing JavaScript packages?
php bin/console importmap:require @hotwired/stimulus, downloads it and registers it in importmap.php. No npm install.
7Cache busting automatic?
Yes. asset-map:compile computes content hashes and renames files. {{ asset() }} always resolves to the current hash.
8When Webpack Encore instead of AssetMapper?
SPAs with React/Vue, IE support through transpilation, complex tree shaking for very large JS bundles. For standard Symfony apps, AssetMapper is the better fit.
9Configuring tailwind.config.js for Symfony?
content: ['./templates/**/*.twig', './assets/**/*.js', './src/**/*.php']. Include every location where Tailwind classes appear.
10Using Alpine.js with AssetMapper?
importmap:require alpinejs, then import and initialize it in assets/app.js. No CDN link, no script tag in the template.