Using Preline UI with Express
A practical guide to using Preline UI JavaScript plugins in Express projects, including static scripts, module bundles, template rendering, cleanup, and optional dependencies.
View guideUpdate v4.2 - New components, 10+ framework guides, and quality improvements. Visit Changelog
A practical guide to wiring Preline UI into Hugo without treating a static site like a JavaScript app.
Hugo renders static HTML, which is the simplest environment for Preline UI. The page already contains the final markup by the time the browser loads JavaScript, so Preline UI can either initialize automatically from a script tag or be initialized explicitly from a small module.
The current Preline UI package supports both styles: the browser script in dist/index.js for full static-page auto initialization, dist/non-auto.mjs for explicit browser-module control, and package imports such as preline/non-auto when Hugo Pipes bundles your JavaScript.
Preline UI is a DOM-driven Tailwind CSS component system. Hugo writes the HTML at build time. Preline UI reads that browser DOM and attaches behavior after the page loads.
That means most Hugo sites do not need route hooks, hydration boundaries, or framework adapters. Keep the markup in Hugo templates and partials, then choose the script-loading style that matches how much JavaScript control your site needs.
The simplest Hugo setup is to copy Preline UI's distribution files into static/vendor/preline and load the browser build near the end of your base layout. This build attaches the plugin classes to the browser environment and initializes on page load.
npm install preline
cp -R node_modules/preline static/vendor/preline
<body>
{{ block "main" . }}{{ end }}
<script src="/vendor/preline/dist/index.js"></script>
</body>
This is the right default for mostly static pages. Hugo gives the browser a complete document, and the Preline UI browser build scans it once after the page load event.
If your Hugo site swaps fragments, lazy-loads sections, or needs targeted initialization, use the module build instead. Import dist/non-auto.mjs from the static Preline copy and call autoInit when the DOM is ready.
<script type="module">
import { HSStaticMethods } from "/vendor/preline/dist/non-auto.mjs";
const initPreline = () => {
HSStaticMethods.autoInit();
};
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", initPreline, { once: true });
} else {
initPreline();
}
document.addEventListener("preline:init", initPreline);
</script>
The custom preline:init event gives other scripts a stable way to ask Preline UI to scan newly inserted markup.
Hugo Pipes uses esbuild for JavaScript bundling, so package imports are a good fit when your Hugo project already has a frontend asset pipeline. In that case, import from preline/non-auto and let Hugo build the module.
import { HSStaticMethods } from "preline/non-auto";
const initPreline = () => {
HSStaticMethods.autoInit();
};
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", initPreline, { once: true });
} else {
initPreline();
}
document.addEventListener("preline:init", initPreline);
{{ $preline := resources.Get "js/preline.ts" | js.Build (dict "target" "es2020" "minify" hugo.IsProduction) | fingerprint }}
<script type="module" src="{{ $preline.RelPermalink }}" integrity="{{ $preline.Data.Integrity }}"></script>
For the whole site, preline/non-auto gives you HSStaticMethods and all plugin classes with explicit timing control. For a plain static copy, the equivalent browser module is /vendor/preline/dist/non-auto.mjs.
HSStaticMethods.autoInit(["dropdown", "overlay"]);
HSStaticMethods.cleanCollection(["dropdown", "overlay"]);
Targeted scans are useful when a Hugo page only contains a small set of interactive components or when another script inserts a known section of markup.
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 is useful when a Hugo section only needs one or two interactive primitives.
npm install @preline/dropdown
import HSDropdown from "@preline/dropdown/non-auto";
document.addEventListener("DOMContentLoaded", () => {
HSDropdown.autoInit();
});
In that single-package setup, the auto entry is import "@preline/dropdown". It is useful for simple static bundles. In shared Hugo assets, /non-auto keeps initialization timing explicit.
autoInit is enough for normal Hugo templates. Manual instances are useful when a custom script owns one specific plugin root and can destroy it before replacing that DOM.
import {
HSDropdown,
type IHTMLElementFloatingUI,
} from "preline/non-auto";
const root = document.querySelector<HTMLDivElement>(".hs-dropdown");
if (root) {
const dropdown = new HSDropdown(
root as unknown as IHTMLElementFloatingUI,
);
window.addEventListener("beforeunload", () => {
dropdown.destroy();
});
}
A plain Hugo page reload gives Preline UI a fresh document every time. Extra cleanup only matters if you use HTMX, Turbo, Swup, search overlays, or another script that injects or removes markup without a page reload.
In that case, re-run autoInit after the fragment is inserted. If you intentionally remove a whole section of initialized markup, clean the relevant collection.
document.dispatchEvent(new Event("preline:init"));
HSStaticMethods.cleanCollection("dropdown");
HSStaticMethods.autoInit("dropdown");
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. Range Slider uses the JavaScript API from noUiSlider. These dependencies do not need to be loaded for dropdowns, overlays, tabs, or tooltips.
If you initialize optional plugins through HSStaticMethods, make the optional library available before the first import of preline/non-auto. The static methods build their plugin map when that module is loaded, and that module can be cached by your Hugo asset bundle.
import noUiSlider from "nouislider";
(
globalThis as typeof globalThis & {
noUiSlider: typeof noUiSlider;
}
).noUiSlider = noUiSlider;
const { HSRangeSlider } = await import("preline/non-auto");
HSRangeSlider.autoInit();
dist/index.js from static/vendor/preline for simple static Hugo pages.dist/non-auto.mjs or preline/non-auto when you need explicit initialization timing.js.Build when you want package imports and a bundled asset.autoInit directly after dynamic fragment swaps.@preline/dropdown/non-auto when a Hugo section only needs one plugin.preline/non-auto import when using HSStaticMethods, or initialize late optional plugins with their direct classes.A practical guide to using Preline UI JavaScript plugins in Express projects, including static scripts, module bundles, template rendering, cleanup, and optional dependencies.
View guideA practical guide to using Preline UI JavaScript plugins in HTML + Vite projects, including direct plugin imports, non-auto timing, manual instances, cleanup, and optional dependencies.
View guide