Using Preline UI with Hugo
A practical guide to using Preline UI JavaScript plugins in Hugo projects, including static scripts, module imports, Hugo Pipes, 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 server-rendered Express pages without exposing your whole node_modules directory.
Express usually serves HTML templates and static assets. That is a natural fit for Preline UI: your view engine renders the Tailwind CSS markup, and Preline UI attaches behavior in the browser after the page loads.
The current Preline UI package supports two practical Express paths: serve the browser script in dist/index.js for ordinary server-rendered pages, or bundle preline/non-auto when you want explicit initialization timing and a smaller public asset surface.
Preline UI is a DOM-driven Tailwind CSS component system. Express renders HTML with EJS, Pug, Handlebars, Nunjucks, or plain responses. Preline UI reads the final browser DOM and wires up components such as dropdowns, overlays, tabs, selects, and tooltips.
There is no framework lifecycle to synchronize with on a normal Express page. Load the script after the markup, or initialize explicitly from a small browser module.
Avoid exposing the entire node_modules directory from Express. Either copy the Preline UI distribution files into your public directory or mount only the Preline package under a narrow static path.
npm install preline
mkdir -p public/vendor/preline
cp -R node_modules/preline/dist public/vendor/preline/dist
import express from "express";
import path from "node:path";
const app = express();
app.use("/assets", express.static(path.join(process.cwd(), "public")));
For standard Express pages, load dist/index.js near the end of your shared layout. This browser build initializes after the page load event.
<body>
<%- body %>
<script src="/assets/vendor/preline/dist/index.js"></script>
</body>
Use this when each navigation returns a complete HTML document. Express sends the markup, the browser loads Preline UI, and the plugin collections start fresh on the new page.
If your Express project already bundles browser assets with esbuild, Vite, Rollup, or Webpack, import preline/non-auto and decide when the scan runs.
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);
{
"scripts": {
"build:js": "esbuild assets/js/preline.ts --bundle --format=esm --platform=browser --outfile=public/js/preline.js"
}
}
For a whole Express site bundle, preline/non-auto gives you HSStaticMethods and named plugin classes without automatic page-load behavior. For a copied static module, the equivalent browser entry is /assets/vendor/preline/dist/non-auto.mjs.
HSStaticMethods.autoInit(["dropdown", "overlay"]);
HSStaticMethods.cleanCollection(["dropdown", "overlay"]);
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 one Express route group only needs one or two interactive primitives.
import HSDropdown from "@preline/dropdown/non-auto";
document.addEventListener("DOMContentLoaded", () => {
HSDropdown.autoInit();
});
autoInit is enough for normal Express templates. Manual instances are useful when a custom browser script owns one 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 normal Express navigation reloads the document, so plugin collections start cleanly. Extra cleanup only matters if you use HTMX, Turbo, PJAX, websockets, or another browser script that injects and removes partial HTML without a page reload.
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 Express browser 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 a public Preline copy for simple server-rendered Express pages.node_modules directory as a public static route.preline/non-auto when you need explicit initialization timing.autoInit directly after dynamic fragment swaps.@preline/dropdown/non-auto when a route group 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 Hugo projects, including static scripts, module imports, Hugo Pipes, 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