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

Laravel

Using Preline UI with Laravel

A practical guide to wiring Preline UI into Laravel through its default Vite entrypoint, including the ES-module ordering trap behind optional dependencies and Blade templates.

Laravel and Preline UI fit together cleanly once you separate the two layers. Laravel is a full-stack PHP framework that renders HTML on the server with Blade and, since the official installer, ships Vite pre-wired for compiling CSS and JavaScript, with no separate build-tool package to add unlike some other server-rendered frameworks. Preline UI is the client-side behavior layer that reads that HTML in the browser and wires up the interaction. The Vite entrypoint your layout loads with @@vite() is what carries Preline UI across that server-to-client boundary.

This guide walks through the integration choices that matter in a real Laravel project: where to initialize Preline UI from the Vite entrypoint, a dependency-ordering trap that's easy to hit and hard to diagnose the first time, how to use Preline UI markup inside Blade views and components, and how to avoid stale plugin references as pages change. If the project also uses Livewire for partial-page updates without full reloads, see the dedicated Using Preline UI with Laravel Livewire guide for the navigation and morph re-init hooks that a vanilla Blade app doesn't need.

Start with the Laravel mental model

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

The detail that matters in Laravel is where the JavaScript runs. Blade templates render server-side HTML, so Preline UI cannot initialize from inside a .blade.php file. It needs the JavaScript entrypoint Vite bundles, typically resources/js/app.js, loaded through the @@vite() Blade directive. A default Laravel install is a traditional multi-page app: every navigation is a full HTTP request and the page reloads, so a single page-load scan covers every visit. That changes the moment you add Livewire, Inertia, or Turbo for partial-page updates, in which case see the Livewire guide linked above.

Initialize Preline UI from the Vite entrypoint

Put initialization in the Vite entrypoint your layout loads through @@vite(), 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, which keeps the pattern consistent if you later add Livewire's navigation and morph hooks.

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

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

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

The Laravel installation guide uses the same preline/non-auto entry and explicit initialization pattern. Keeping the HSStaticMethods module binding in hand also makes the later single-plugin imports, manual instances, and scoped autoInit calls easier to follow.

The root preline entry and preline/non-auto are genuinely different builds. The root entry registers per-plugin window load listeners, while the non-auto entry omits them. Because this guide initializes explicitly, preline/non-auto keeps timing under Laravel's control and avoids a second automatic initialization path.

Livewire, Inertia, and other partial-update tools

A vanilla Blade app has no DOM-patching navigation to work around: every link click and form submission is a full page load, the entrypoint re-runs, and autoInit covers the fresh DOM. That's the whole story, and it's worth appreciating: some frameworks in this position have a DOM-diffing navigation layer that patches content in place using a same-tag-name match instead of a real reload, and can silently merge JavaScript-inserted markup (for example, a Datatable's pagination buttons) into unrelated elements on the next page if two nodes share a bare marker attribute with no distinguishing value. A default Laravel project has no equivalent problem to solve.

That changes as soon as something in the stack starts patching the DOM instead of reloading it. Livewire is the most common case in a Laravel app: it morphs components in place across server round-trips and, with wire:navigate, does SPA-style navigation without a full reload either. Both replace nodes Preline UI already initialized, so a single page-load autoInit stops being enough. The Laravel Livewire guide covers the exact hooks (livewire:navigated and the morphed hook) plus wire:ignore for widgets whose open/selected state you don't want a re-render to reset. If the project uses Inertia or Turbo instead, apply the same principle: find that tool's own "content was just swapped in" event and re-run autoInit from it.

Choose imports by the level of control you need

For a full Preline UI installation, preline/non-auto is the best Laravel default. It gives you HSStaticMethods and the plugin classes without depending on auto-entry page-load timing, which matters once anything other than a plain full-page navigation is in play.

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 Laravel surfaces focused

Not every Laravel 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";

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

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

Use Preline UI markup in Blade templates

Blade only emits HTML, so Preline UI markup drops straight into a .blade.php file with the same Tailwind CSS classes you would use anywhere else. Where Laravel's own conventions add value is repeated per-instance markup: an accordion item, a datatable row, a datepicker input reused with different options. A Blade component (resources/views/components/*.blade.php, invoked as <x-accordion-item />) is the idiomatic place for that, once a second real usage justifies factoring it out, not before.

resources/views/components/accordion-item.blade.php
                        
                          @props(['title'])

                          <div class="hs-accordion">
                            <button class="hs-accordion-toggle ...">
                              {{ $title }}
                            </button>
                            <div class="hs-accordion-content hidden w-full overflow-hidden transition-[height] duration-300">
                              <p class="text-gray-800 dark:text-gray-200">{{ $slot }}</p>
                            </div>
                          </div>
                        
                      

Invoke it with Blade's component syntax, and JSON-shaped plugin options (Datepicker's data-hs-datepicker, Datatable's data-hs-datatable, Range Slider's data-hs-range-slider) the same way: build the array in a @@php block and pass it through json_encode(), then let Blade's normal {{ }} escaping handle the attribute. It produces the same &quot;-encoded output any other server-side templating language's auto-escaping would, so there's no special case to reach for.

resources/views/pages/accordion.blade.php
                        
                          <div class="hs-accordion-group">
                            <x-accordion-item title="Accordion #1">
                              Body copy for the first item.
                            </x-accordion-item>
                            <x-accordion-item title="Accordion #2">
                              Body copy for the second item.
                            </x-accordion-item>
                          </div>
                        
                      

Make sure the shared layout every page extends loads the entrypoint with @@vite(['resources/css/app.css', 'resources/js/app.js']), so every page that renders Preline UI markup also ships the script that initializes it.

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. 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("DOMContentLoaded", initUserMenu);
                        
                      

The getInstance guard keeps a single instance per node. If Livewire or another tool is allowed to replace the node, prefer page-level autoInit triggered from that tool's own re-init hook over a long-lived manual instance, since the element it points at can be replaced out from under it.

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

A full page load resets the registry, so a plain Blade app needs nothing extra. If something in the stack removes a region of Preline UI markup without a full reload (a Livewire morph, an Inertia visit), 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 and the ES-module ordering trap

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

Four plugins do reach for an optional third-party dependency, and each checks for it as a global at the moment preline/non-auto itself is evaluated, not lazily inside autoInit(). Datatable needs window.jQuery (from datatables.net's own dependency on jQuery) and window.DataTable (the constructor itself, referenced as a bare global inside Preline UI's own plugin code, not through an import). Datepicker needs window.VanillaCalendarPro and, less obviously, window._ (lodash) for a _.mergeWith() call buried in its options-merging logic, not visible from its public API. File Upload needs window.Dropzone and reuses the same window._ lodash requirement. Range Slider needs only window.noUiSlider, with no second dependency, because unlike the other three, Preline UI's own source imports nouislider's types only (import type, erased at compile time, nothing bundled), so the plugin genuinely has no bundled fallback and no lodash-equivalent second requirement either. If any of these globals is missing at that exact moment, its plugin is disabled for the rest of the page's JS session, and installing 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, then preline/non-auto) fully evaluate first, in that order. Preline UI's dependency gate resolves while evaluating, before app.js's own window.VanillaCalendarPro = ... line ever runs. This is straightforward to confirm once you know to look: the collection Preline UI creates for the plugin (window.$hsDatepickerCollection, for Datepicker) exists but stays empty, and a manual HSStaticMethods.autoInit() call afterward doesn't populate it either, since the gate already resolved false, permanently, for this page load. 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 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. vendor-globals.js's position relative to preline/non-auto is what matters; the order of the assignments inside it does not, since the whole module finishes evaluating before preline/non-auto does either way.

Range Slider's Tailwind CSS classes are fully custom through the cssClasses option, so you do not need to bring in noUiSlider's own CSS just to make Preline UI styling work. Datepicker's calendar popup styling is separate from variants.css; import the public preline/datepicker-styles-utility.css export when you use it.

The practical checklist

  • Initialize Preline UI from your Vite entrypoint such as resources/js/app.js, loaded through @@vite(), not from a .blade.php template.
  • Use preline/non-auto and run HSStaticMethods.autoInit() once the DOM is ready, leaving initialization timing under the application's control.
  • A default Laravel project reloads the full page on every navigation, so one page-load autoInit is enough. If Livewire, Inertia, or Turbo is in the stack, re-run autoInit from that tool's own re-init event; see the Livewire guide for the exact hooks.
  • Guard manual instances with getInstance, and call cleanCollection when you intentionally remove a region of initialized markup.
  • Datatable and Datepicker each need two globals (jQuery + DataTable; VanillaCalendarPro + lodash); File Upload needs Dropzone plus the same lodash global; Range Slider needs only noUiSlider, with no second dependency, because Preline UI only imports its types, not its runtime code.
  • Set those globals in a dedicated module imported before preline/non-auto, not inline in the same file, or Preline UI's dependency gate resolves before your assignment runs, and the plugin stays disabled for the rest of the page's JS session no matter what you do afterward.

© 2026 Preline Labs.