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

Laravel Livewire

Using Preline UI with Laravel Livewire

A practical guide to wiring Preline UI into Laravel Livewire so plugins keep working as components morph the DOM across server round-trips.

Laravel Livewire and Preline UI fit together cleanly once you separate the two layers. Livewire is a full-stack Laravel framework that renders Blade components on the server and keeps them alive across HTTP round-trips, swapping markup in place by morphing the DOM instead of reloading the page. Preline UI is the client-side behavior layer that reads that HTML in the browser and wires up the interaction.

The detail that matters in Livewire is that every component update can destroy and recreate DOM nodes. Preline UI attaches behavior to the nodes that exist when it initializes, so after Livewire morphs the page you have to run autoInit again. This guide covers where to initialize from your Vite entrypoint, how to re-initialize after Livewire navigation and morphs, how to use Preline UI markup in .blade.php files, and how to avoid stale plugin references as components update.

Start with the Livewire mental model

Preline UI is a DOM-driven Tailwind CSS component system. Livewire 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 Livewire, plain HTML, React, Vue, Svelte, AdonisJS, Rails, and similar stacks.

The detail that matters in Livewire is timing. Blade renders server-side HTML, so Preline UI cannot initialize from inside a .blade.php file. It needs a client script that Vite bundles from your resources/js entrypoint. And because Livewire morphs the DOM on each update rather than doing a full reload, a single page-load scan is not enough. Everything below is about running initialization at the right moment for each kind of update.

Initialize Preline UI from the Vite entrypoint

Put initialization in the Vite entrypoint that your layout loads through the @@vite directive, usually resources/js/app.js. Vite bundles that file, so module imports work as expected. preline/non-auto is a good default here: nothing runs until your code decides the DOM is ready.

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

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

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

This handles the first full page load. It is not enough on its own, because Livewire updates the page without reloading the entrypoint. The next section adds the hooks that re-run autoInit after Livewire changes the DOM.

Reinitialize after Livewire navigation and morphs

This is the key step for Livewire. Livewire v3 changes the page in two ways without a full reload: SPA-style navigation through wire:navigate, and component morphs after each network round-trip. Both can replace nodes that Preline UI initialized, so re-run autoInit after each. The same resources/js/app.js entrypoint is the place to register these hooks.

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

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

                          // First load and every wire:navigate visit
                          document.addEventListener("livewire:navigated", autoInit);

                          // After a component morphs its DOM
                          document.addEventListener("livewire:init", () => {
                            Livewire.hook("morphed", () => {
                              autoInit();
                            });
                          });
                        
                      

livewire:navigated also fires on the initial page load when wire:navigate is in use, so it doubles as your first-load hook. Register the morphed hook inside livewire:init so Livewire's global is available, and let it run autoInit after each component finishes morphing. autoInit skips elements that already have plugin instances, so re-running it after every update is safe.

Know the difference between Livewire v2 and v3

The hooks above target Livewire v3, which ships wire:navigate and the morphed JavaScript hook. Livewire v2 has no SPA navigation and uses different events: livewire:load fires once on boot, and the message.processed hook fires after each component re-render.

resources/js/app.js (Livewire v2)
                        
                          import { HSStaticMethods } from "preline/non-auto";

                          document.addEventListener("livewire:load", () => {
                            HSStaticMethods.autoInit();

                            Livewire.hook("message.processed", () => {
                              HSStaticMethods.autoInit();
                            });
                          });
                        
                      

Prefer the v3 hooks on new projects. The morphed hook is more precise than the broad v2 update cycle, and livewire:navigated keeps Preline UI working across wire:navigate visits that v2 does not have.

Choose imports by the level of control you need

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

resources/js/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.

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

                          HSDropdown.autoInit();
                        
                      

Single plugin packages keep small Livewire surfaces focused

Not every Livewire 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
                        
                      
resources/js/app.js
                        
                          import HSDropdown from "@preline/dropdown/non-auto";

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

                          document.addEventListener("livewire:navigated", autoInit);
                          document.addEventListener("livewire:init", () => {
                            Livewire.hook("morphed", autoInit);
                          });
                        
                      

In that single-package setup, the auto entry is import "@preline/dropdown". Use it only for static pages with no Livewire updates. As soon as morphs or wire:navigate are involved, the /non-auto entry keeps initialization aligned with the timing you control.

Use Preline UI markup in Blade templates

Blade and Livewire templates only emit HTML, so Preline UI markup drops straight into a .blade.php file with the same Tailwind CSS classes you would use anywhere else. The Vite entrypoint your layout loads is what attaches the behavior once the page reaches the browser.

resources/views/livewire/actions-menu.blade.php
                        
                          <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 shared layout loads the entrypoint with the Vite directive, for example @@vite(['resources/js/app.js']), 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 Livewire to reset on re-render, wrap it in wire:ignore so Livewire skips morphing that subtree.

resources/views/livewire/actions-menu.blade.php
                        
                          <div wire:ignore>
                            <div class="hs-dropdown relative inline-flex">
                              ...
                            </div>
                          </div>
                        
                      

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 wire:ignore so Livewire does not morph it away. Create the instance from your entrypoint and guard it with getInstance so a re-run does not double-initialize the same node.

resources/js/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("livewire:navigated", initUserMenu);
                          document.addEventListener("livewire:init", () => {
                            Livewire.hook("morphed", initUserMenu);
                          });
                        
                      

The getInstance guard keeps a single instance per node across morphs. If Livewire is allowed to morph the node, prefer page-level autoInit over a long-lived manual instance, since the element it points at can be replaced.

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, Livewire, and other environments without framework context.

A full page load resets the registry, but Livewire morphs replace nodes in place. When a morph removes initialized markup, its instance can linger in the collection as a stale reference. When you intentionally swap out 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.

Datepicker uses vanilla-calendar-pro. File Upload uses dropzone plus lodash. Both plugins are disabled the same way Datatable is when their dependency is missing.

These dependency checks run once, when preline/non-auto itself is evaluated, not lazily inside autoInit(). If the global a plugin needs (window.jQuery, window.DataTable, window.VanillaCalendarPro, window.noUiSlider, window.Dropzone) is not set at that exact instant, that plugin is disabled for the rest of the page's JS session, and loading the library afterward and calling autoInit() again does not recover it.

Set these globals in a dedicated module that you import before preline/non-auto, not inline in the same file, even textually above the preline/non-auto import. ES modules fully evaluate each sibling import, including that sibling's own top-level code and not just its own imports, before the importing module's own statements run. So writing this directly in app.js does not work, even though the assignment reads "before" the Preline import:

resources/js/app.js (does not work)
                        
                          import * as VanillaCalendarPro from 'vanilla-calendar-pro';
                          window.VanillaCalendarPro = VanillaCalendarPro; // too late

                          import { HSStaticMethods } from 'preline/non-auto';
                        
                      

Both imported modules, vanilla-calendar-pro and then preline/non-auto, fully evaluate first, in that order. Preline's dependency gate resolves while evaluating, before app.js's own window.VanillaCalendarPro = ... line ever runs. Put the assignment in its own module instead, and import that module first:

resources/js/vendor-globals.js
                        
                          import _ from 'lodash';
                          import * as VanillaCalendarPro from 'vanilla-calendar-pro';
                          import jQuery from 'jquery';
                          import DataTable from 'datatables.net';
                          import * as noUiSlider from 'nouislider';
                          import { Dropzone } from 'dropzone';

                          window._ = _;
                          window.VanillaCalendarPro = VanillaCalendarPro;
                          window.jQuery = window.$ = jQuery;
                          window.DataTable = DataTable;
                          window.noUiSlider = noUiSlider;
                          window.Dropzone = Dropzone;
                        
                      
resources/js/app.js
                        
                          // Must be imported before preline/non-auto
                          import './vendor-globals';

                          import { HSStaticMethods } from 'preline/non-auto';
                        
                      

Only include the imports for the plugins your pages actually use. Only vendor-globals.js's position relative to preline/non-auto matters; the order of the assignments inside it does not, since the whole module finishes evaluating before preline/non-auto does either way.

The practical checklist

  • Initialize Preline UI from your Vite entrypoint such as resources/js/app.js, not from a .blade.php template.
  • Use preline/non-auto and run HSStaticMethods.autoInit() on first load, on livewire:navigated, and inside the morphed hook so Livewire updates keep working.
  • On Livewire v2, fall back to livewire:load plus the message.processed hook, since wire:navigate and morphed are v3.
  • Wrap stateful Preline UI widgets in wire:ignore when you do not want Livewire to morph them on re-render.
  • 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, noUiSlider for Range Slider, vanilla-calendar-pro for Datepicker, or dropzone for File Upload.
  • Set those dependencies' globals (window.jQuery, window.DataTable, window.VanillaCalendarPro, window.noUiSlider, window.Dropzone) in a dedicated module imported before preline/non-auto, not inline in the same file, or Preline's dependency gate resolves before your assignment runs.

© 2026 Preline Labs.