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

AdonisJS

Using Preline UI with AdonisJS

A practical guide to wiring Preline UI into AdonisJS across Edge templates and Inertia without fighting its server-first rendering.

AdonisJS and Preline UI fit together cleanly once you separate the two layers. AdonisJS is a full-stack MVC Node.js framework that renders HTML on the server with the Edge templating engine and bundles frontend assets through Vite. 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 you load through Vite is what carries Preline UI across that server-to-client boundary.

This guide walks through the integration choices that matter in a real AdonisJS project: where to run autoInit from the Vite entrypoint, how to keep Preline UI working across Edge page loads and Inertia navigation, how to use Preline UI markup inside .edge templates, and how to avoid stale plugin references as pages change.

Start with the AdonisJS mental model

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

The detail that matters in AdonisJS is where the JavaScript runs. Edge templates render server-side HTML, so Preline UI cannot initialize from inside a .edge file. It needs a client script that @adonisjs/vite bundles and ships from your resources/js entrypoint. When you render with Inertia and Vue instead, initialization moves into the Vue lifecycle. Everything below is about putting initialization in the right place for each case.

Initialize Preline UI from the Vite entrypoint

For Edge-rendered pages, put initialization in the Vite entrypoint that your layout loads through the @@vite tag, usually resources/js/app.js. Vite bundles that file, so module imports work as expected.

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

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

preline/non-auto is the right default here because every page calls autoInit() explicitly. The default preline entry registers per-plugin window load listeners, while preline/non-auto does not. The non-auto entry therefore leaves initialization timing under the application's control and avoids a second automatic initialization path.

Since Edge views are already rendered markup by the time this module runs, the explicit call on DOMContentLoaded is what actually wires the plugins up. Edge navigation is a full page load, so the entrypoint re-runs and autoInit scans the fresh DOM on every page. During development, a Vite HMR update reloads the module, and autoInit skips nodes that already have plugin instances, so re-running it is safe.

Load optional dependencies before Preline UI

Datatable, Datepicker, Range Slider, and File Upload each check for a global on window (jQuery, DataTable, VanillaCalendarPro, noUiSlider, Dropzone) the moment Preline UI's module evaluates. If the global is not there yet, the plugin is disabled for that page load; loading the dependency afterward and calling autoInit() again does not recover it. All of the globals a page might need have to be assigned before preline is imported, not lazily per page.

Put those assignments in a separate module and import that module before preline in your entrypoint. This matters more than it looks: within a single file, ES modules evaluate all of that file's own static imports as one batch before any of the file's own top-level statements run, so writing import jQuery from "jquery"; window.jQuery = jQuery; import "preline"; in one file does not reliably run the assignment before preline's module evaluates. A sibling module only gets its own evaluation slot, with a guaranteed order relative to the next import, when it lives in its own file.

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

                          window.jQuery = jQuery;
                          window.$ = jQuery;
                          window.DataTable = DataTable;
                          window._ = _;
                          window.VanillaCalendarPro = VanillaCalendarPro;
                          window.noUiSlider = noUiSlider;
                          window.Dropzone = Dropzone;
                        
                      
resources/js/app.js
                        
                          import "./vendor-globals.js";
                          import { HSStaticMethods } from "preline/non-auto";

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

Not using a plugin that needs one of these globals? Drop its import and assignment from vendor-globals.js, since the rest of Preline UI does not depend on it.

Use Preline UI markup in Edge templates

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

resources/views/pages/home.edge
                        
                          <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 AdonisJS Vite tag, for example @@vite(['resources/js/app.js']), so every Edge page that renders Preline UI markup also ships the script that initializes it.

Using the Inertia starter kit instead

Everything above assumes the web starter kit, where AdonisJS renders full pages with Edge. If you scaffolded your project with npm init adonisjs@latest <project-name> -- --kit=inertia instead, pages render through Inertia and a client-side framework such as Vue, and a single entrypoint scan on first load is not enough. The sections below cover what changes.

Reinitialize on Inertia navigation

When AdonisJS renders with Inertia and Vue 3, pages swap on the client without a full reload, so a single entrypoint scan is not enough. Register a Vue plugin from your Inertia entry that runs autoInit after each component mounts, once Vue has flushed the DOM.

inertia/plugins/preline.ts
                        
                          import { nextTick } from "vue";
                          import type { App } from "vue";
                          import { HSStaticMethods } from "preline/non-auto";

                          export default {
                            install(app: App) {
                              app.mixin({
                                async mounted() {
                                  await nextTick();
                                  HSStaticMethods.autoInit();
                                },
                              });
                            },
                          };
                        
                      

Register the plugin where you create the Inertia app in inertia/app/app.ts with .use(PrelinePlugin). nextTick waits for Vue's DOM flush, and autoInit skips elements that already have plugin instances, so running it again after each Inertia visit is safe.

Use manual instances when a component owns the DOM node

autoInit is good for page-level markup. Manual instances are better when an Inertia Vue component owns one specific plugin root and can clean it up directly. This is especially useful for reusable components and conditionally rendered overlays.

inertia/components/Dropdown.vue
                        
                          <script setup lang="ts">
                            import { onMounted, onBeforeUnmount, ref } from "vue";
                            import { HSDropdown, type IHTMLElementFloatingUI } from "preline/non-auto";

                            const root = ref<HTMLDivElement | null>(null);
                            let dropdown: HSDropdown | null = null;

                            onMounted(() => {
                              if (!root.value) return;
                              dropdown = new HSDropdown(root.value as unknown as IHTMLElementFloatingUI);
                            });

                            onBeforeUnmount(() => {
                              dropdown?.destroy();
                            });
                          </script>

                          <template>
                            <div ref="root" class="hs-dropdown relative inline-flex">
                              ...
                            </div>
                          </template>
                        
                      

Choose imports by the level of control you need

preline/non-auto from the entrypoint shown earlier already covers most AdonisJS pages. Reach further into it when you want to scope initialization, for example initializing only a subset of plugins, or reinitializing a specific plugin's collection after removing markup from the DOM.

resources/js/preline.ts
                        
                          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 TypeScript surface while still letting your code decide when to initialize.

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

                          HSDropdown.autoInit();
                        
                      

Single plugin packages keep small AdonisJS surfaces focused

Not every AdonisJS 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 for simple Edge pages. When Inertia client navigation is involved, the /non-auto entry keeps initialization aligned with the timing you control.

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

For Edge pages, full reloads mean the registry resets on each navigation, so page-level autoInit is usually all you need. With Inertia, content swaps without a reload: when a component creates a manual instance, destroy it on unmount, and when you intentionally remove a whole group of initialized markup, clean the relevant collection.

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 Vite entrypoint such as resources/js/app.js, not from a .edge template.
  • Use preline/non-auto and call HSStaticMethods.autoInit() once the DOM is ready so Edge page loads pick up Preline UI markup. The non-auto entry skips the default entry's per-plugin window load listeners and leaves initialization timing under the application's control.
  • Assign optional third-party globals (jQuery, DataTable, noUiSlider, VanillaCalendarPro, Dropzone) in their own module, imported before preline, not inline in the same file as the preline import.
  • With the Inertia starter kit and Vue, register a plugin that runs autoInit after nextTick on each mount so client navigation keeps working.
  • Create manual plugin instances when an Inertia component owns one specific node, and call destroy() on unmount.
  • 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.