Update v5.0 - Preline MCP, AI Prompts, Animated Icons and more. Visit Changelog

Symfony

Using Preline UI with Symfony

A practical guide to wiring Preline UI into Symfony across Webpack Encore and AssetMapper, including the Turbo Drive re-init hook for projects that use Symfony UX Turbo.

Symfony and Preline UI fit together cleanly once you separate the two layers. Symfony is a full-stack PHP MVC framework that renders HTML on the server with the Twig templating engine and manages frontend assets through Webpack Encore or AssetMapper. Preline UI is the client-side behavior layer that reads that HTML in the browser and wires up the interaction. The small amount of script your asset pipeline ships is what carries Preline UI across that server-to-client boundary.

This guide walks through the integration choices that matter in a real Symfony project: where to run autoInit from a Webpack Encore entry, how the same idea maps to AssetMapper, how to keep Preline UI working when Symfony UX Turbo swaps the DOM without a reload, how to use Preline UI markup inside .html.twig templates, and how to avoid stale plugin references as pages change.

Start with the Symfony mental model

Preline UI is a DOM-driven Tailwind CSS component system. Symfony produces the markup, and Preline UI attaches behavior to that markup once it exists in the browser. That is why the same dropdown, overlay, tabs, tooltip, or select markup can move between Symfony, plain HTML, React, Vue, Svelte, Laravel, Rails, and similar stacks.

The detail that matters in Symfony is where the JavaScript runs. Twig templates render server-side HTML, so Preline UI cannot initialize from inside a .html.twig file. It needs a client script that your asset pipeline ships, either a Webpack Encore entry such as assets/app.js or an AssetMapper entry loaded through importmap(). A plain Symfony page is a full reload, so a single page-load scan is enough. As soon as Symfony UX Turbo is in play, navigation becomes a partial DOM swap and you have to re-run initialization. Everything below is about putting autoInit in the right place for each case.

Initialize Preline UI from a Webpack Encore entry

Webpack Encore is the most common Symfony frontend setup. After npm install preline, import Preline UI from your Encore entry, usually assets/app.js. Encore bundles that file with Webpack, so module imports work as expected. preline/non-auto is a good default here: nothing runs until your code decides the DOM is ready.

assets/app.js
                        
                          import "./styles/app.css";
                          import { HSStaticMethods } from "preline/non-auto";

                          function autoInit() {
                            HSStaticMethods.autoInit();
                          }

                          if (document.readyState === "loading") {
                            document.addEventListener("DOMContentLoaded", autoInit);
                          } else {
                            autoInit();
                          }
                        
                      

Bring the Tailwind CSS entry that builds Preline UI styling into the same Encore entry, for example assets/styles/app.css, so the bundle ships both the markup behavior and the styles. Then load the Encore entry from your base layout with the asset functions.

templates/base.html.twig
                        
                          {% block stylesheets %}
                            {{ encore_entry_link_tags('app') }}
                          {% endblock %}

                          {% block javascripts %}
                            {{ encore_entry_script_tags('app') }}
                          {% endblock %}
                        
                      

Standard Symfony navigation is a full page load, so the entry re-runs and autoInit scans the fresh DOM on every page. autoInit also skips nodes that already have plugin instances, so re-running it is always safe.

Use AssetMapper when there is no Node.js build step

AssetMapper is the modern Symfony asset pipeline that ships ES modules over an importmap with no Webpack bundling. AssetMapper reads from local asset directories rather than node_modules, so the install copies the required Preline UI files into assets/vendor/preline/ and maps them in importmap.php. The full Symfony AssetMapper installation covers that setup step by step.

With the importmap in place, initialize Preline UI from your AssetMapper entry. Because AssetMapper maps the preline bundle, use the window.HSStaticMethods global it exposes and run autoInit once the DOM is ready.

assets/app.js
                        
                          import "./styles/app.css";
                          import "preline";

                          document.addEventListener("DOMContentLoaded", () => {
                            window.HSStaticMethods.autoInit();
                          });
                        
                      

Load the entry by rendering the importmap in your base Twig layout with {{ importmap('app') }}. The rest of this guide applies the same way to both pipelines: Encore exposes the plugin classes through bundled module imports, while AssetMapper exposes window.HSStaticMethods from the mapped bundle.

Reinitialize on Turbo Drive navigation

This is the key step for any project that uses symfony/ux-turbo. Turbo Drive intercepts link clicks and form submissions and replaces the page <body> with a partial DOM swap instead of doing a full reload. Your entry script runs once, but DOMContentLoaded does not fire again on later Turbo visits, so Preline UI markup rendered after navigation never gets initialized. This is the same re-init problem you see in Livewire, and the fix is the same shape: re-run autoInit after each navigation.

Hook into turbo:load instead of DOMContentLoaded. Turbo fires turbo:load on the initial page load and after every Turbo visit, so it doubles as your first-load hook and your re-init hook.

assets/app.js
                        
                          import "./styles/app.css";
                          import { HSStaticMethods } from "preline/non-auto";

                          // Fires on first load and after every Turbo visit
                          document.addEventListener("turbo:load", () => {
                            HSStaticMethods.autoInit();
                          });
                        
                      

If you also render lazy <turbo-frame> regions, those load their own markup independently, so re-run autoInit after a frame loads as well. autoInit skips elements that already have plugin instances, so running it on both events keeps the whole page covered without double-initializing.

assets/app.js
                        
                          // After a lazy Turbo Frame swaps in its content
                          document.addEventListener("turbo:frame-load", () => {
                            HSStaticMethods.autoInit();
                          });
                        
                      

Choose imports by the level of control you need

For a full Preline UI installation under Webpack Encore, preline/non-auto is the best default. It gives you HSStaticMethods and the plugin classes without depending on plugin auto-entry page-load timing, which matters once Turbo controls when the DOM changes.

assets/preline.js
                        
                          import { HSStaticMethods } from "preline/non-auto";

                          HSStaticMethods.autoInit(["dropdown", "overlay"]);
                          HSStaticMethods.cleanCollection(["dropdown", "overlay"]);
                        
                      

If a page only needs the class for one plugin from the full package, import that class from preline/non-auto. This keeps the example aligned with the package's declared surface while still letting your code decide when to initialize.

assets/dropdown.js
                        
                          import { HSDropdown } from "preline/non-auto";

                          HSDropdown.autoInit();
                        
                      

Single plugin packages keep small Symfony surfaces focused

Not every Symfony app needs the whole library. Preline UI plugins can also be consumed from single-plugin dependencies when those packages are available in your dependency set, for example @preline/dropdown, @preline/overlay, @preline/select, or @preline/range-slider. This keeps the integration focused when only a small part of the page needs Preline UI behavior.

Terminal
                        
                          npm install @preline/dropdown
                        
                      
assets/app.js
                        
                          import HSDropdown from "@preline/dropdown/non-auto";

                          document.addEventListener("turbo:load", () => {
                            HSDropdown.autoInit();
                          });
                        
                      

In that single-package setup, the auto entry is import "@preline/dropdown". Use it only for plain Symfony pages with full reloads. As soon as Turbo navigation is involved, the /non-auto entry keeps initialization aligned with the timing you control.

Use Preline UI markup in Twig templates

Twig templates only emit HTML, so Preline UI markup drops straight into a .html.twig file with the same Tailwind CSS classes you would use anywhere else. The Encore or AssetMapper entry your layout loads is what attaches the behavior once the page reaches the browser.

templates/components/actions_menu.html.twig
                        
                          <div class="hs-dropdown relative inline-flex">
                            <button id="hs-dropdown-example" type="button" class="hs-dropdown-toggle ..." aria-haspopup="menu" aria-expanded="false" aria-label="Dropdown">
                              Actions
                            </button>

                            <div class="hs-dropdown-menu ... hidden" role="menu" aria-orientation="vertical" aria-labelledby="hs-dropdown-example">
                              ...
                            </div>
                          </div>
                        
                      

Make sure your base layout loads the entry that bundles Preline UI, for example {{ encore_entry_script_tags('app') }} under Encore or {{ importmap('app') }} under AssetMapper, so every page that renders Preline UI markup also ships the script that initializes it. When a Preline UI widget holds open or selected state you do not want Turbo to reset on navigation, mark the element with data-turbo-permanent and a stable id so Turbo preserves that subtree across visits.

Use manual instances when you own a specific node

autoInit is good for page-level markup. A manual instance is better when one specific node needs explicit control, especially when it is paired with data-turbo-permanent so Turbo keeps it across visits. Create the instance from your entry and guard it with getInstance so a re-run does not double-initialize the same node.

assets/app.js
                        
                          import { HSDropdown } from "preline/non-auto";

                          function initUserMenu() {
                            const el = document.querySelector("#user-menu");
                            if (el && !HSDropdown.getInstance(el)) {
                              new HSDropdown(el);
                            }
                          }

                          document.addEventListener("turbo:load", initUserMenu);
                        
                      

The getInstance guard keeps a single instance per node across Turbo visits. If Turbo is allowed to replace the node, prefer page-level autoInit over a long-lived manual instance, since the element it points at can be swapped out.

Cleanup matters because Preline UI keeps registries

Preline UI stores plugin instances in internal collections such as window.$hsDropdownCollection. That registry lets plugins coordinate in plain HTML, Symfony, and other environments without framework context.

A full page load resets the registry, so plain Symfony navigation needs nothing extra. Turbo Drive replaces the <body> without a reload, so a registry entry can linger as a stale reference when its markup is swapped out. When you intentionally remove a region of Preline UI markup, clean the relevant collection before re-running autoInit so the registry only holds live nodes.

Cleanup
                        
                          HSStaticMethods.cleanCollection("dropdown");
                          HSStaticMethods.cleanCollection(["dropdown", "overlay"]);
                        
                      

Optional dependencies only matter for the plugins that use them

Most core plugins do not use jQuery. Dropdowns, overlays, tooltips, popovers, tabs, and similar components use plain JavaScript. Positioning behavior uses @floating-ui/dom.

jQuery is only relevant for Datatable because datatables.net depends on it. If jQuery and DataTables are not present, Datatable should not initialize. That dependency does not affect dropdowns, overlays, tabs, or tooltips.

Range Slider uses the JavaScript API from noUiSlider. Preline UI remains responsible for the Tailwind CSS markup and behavior wrapper, so you do not need to bring in noUiSlider CSS just to make Preline UI styling work.

The practical checklist

  • Initialize Preline UI from your asset entry such as assets/app.js, not from a .html.twig template.
  • Under Webpack Encore, use preline/non-auto and run HSStaticMethods.autoInit() once the DOM is ready; under AssetMapper, map the preline bundle and call window.HSStaticMethods.autoInit().
  • If the project uses symfony/ux-turbo, run autoInit on turbo:load instead of DOMContentLoaded, and on turbo:frame-load for lazy Turbo Frames.
  • Mark stateful Preline UI widgets with data-turbo-permanent and a stable id when you do not want Turbo to reset them across visits.
  • Guard manual instances with getInstance, and call cleanCollection when you intentionally remove a region of initialized markup.
  • Install optional third-party dependencies only for the plugins that need them, such as datatables.net for Datatable or noUiSlider for Range Slider.

© 2026 Preline Labs.