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

Hugo

Using Preline UI with Hugo

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.

Start with the Hugo mental model

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.

Use the static script for normal Hugo pages

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.

Terminal
                        
                          npm install preline
                          cp -R node_modules/preline static/vendor/preline
                        
                      
layouts/baseof.html
                        
                          <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. Note the path: Hugo's template system moved base layouts to the project root in v0.146.0, so this is layouts/baseof.html, not layouts/_default/baseof.html.

Use non-auto modules when you want timing control

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.

layouts/_partials/preline.html
                        
                          <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.

Bundle Preline UI with Hugo Pipes

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.

assets/js/preline.ts
                        
                          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);
                        
                      
layouts/_partials/scripts.html
                        
                          {{ $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>
                        
                      

Choose imports by page scope

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.

Targeted scan
                        
                          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.

Single plugin packages keep small Hugo pages focused

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.

Terminal
                        
                          npm install @preline/dropdown
                        
                      
assets/js/dropdown.ts
                        
                          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.

Use manual instances for custom scripts

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.

assets/js/dropdown.ts
                        
                          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();
                            });
                          }
                        
                      

Handle dynamic fragments intentionally

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.

Fragment update
                        
                          document.dispatchEvent(new Event("preline:init"));

                          HSStaticMethods.cleanCollection("dropdown");
                          HSStaticMethods.autoInit("dropdown");
                        
                      

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.

Four plugins rely on optional third-party libraries, each gated behind its own availability check:

  • Datatable needs both jquery and a DataTables build (e.g. datatables.net-dt), since datatables.net itself depends on jQuery.
  • Datepicker needs vanilla-calendar-pro and lodash. This second dependency is easy to miss: the plugin's own availability check only verifies vanilla-calendar-pro, but the plugin internally calls into lodash regardless. Skipping lodash passes the availability check and then throws at runtime the first time a datepicker actually renders.
  • File Upload needs both lodash and dropzone, and this pair is fully covered by its availability check.
  • Range Slider needs only noUiSlider.

None of these need to be loaded for dropdowns, overlays, tabs, tooltips, or any of the other dependency-free plugins.

If you initialize optional plugins through HSStaticMethods, make every optional library available on window before the first import of preline/non-auto. The static methods build their plugin availability map once, at that module's load time, so a library that becomes available afterwards will not retroactively enable its plugin. That module can also be cached by your Hugo asset bundle across page loads.

assets/js/preline.ts
                        
                          import $ from "jquery"; // Datatable
                          import _ from "lodash"; // Datatable, Datepicker, File Upload
                          import Dropzone from "dropzone"; // File Upload
                          import noUiSlider from "nouislider"; // Range Slider

                          window.$ = $;
                          window.jQuery = $;
                          window._ = _;
                          window.Dropzone = Dropzone;
                          window.noUiSlider = noUiSlider;

                          async function initPreline() {
                            const dtModule = await import("datatables.net-dt"); // Datatable
                            window.DataTable = dtModule.default ?? dtModule;

                            const vcModule = await import("vanilla-calendar-pro"); // Datepicker
                            window.VanillaCalendarPro = vcModule.Calendar ?? vcModule.default ?? vcModule;

                            const { HSStaticMethods } = await import("preline/non-auto");
                            HSStaticMethods.autoInit();
                          }

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

A single-plugin import only needs its own dependency, following the same rule: the library must land on window before the plugin's module is imported.

assets/js/range-slider.ts
                        
                          import noUiSlider from "nouislider";

                          (
                            globalThis as typeof globalThis & {
                              noUiSlider: typeof noUiSlider;
                            }
                          ).noUiSlider = noUiSlider;

                          const { HSRangeSlider } = await import("preline/non-auto");

                          HSRangeSlider.autoInit();
                        
                      

The practical checklist

  • Use dist/index.js from static/vendor/preline for simple static Hugo pages.
  • Use dist/non-auto.mjs or preline/non-auto when you need explicit initialization timing.
  • Use Hugo Pipes with js.Build when you want package imports and a bundled asset. Base layouts and partials live at layouts/baseof.html and layouts/_partials/ (not layouts/_default/ or layouts/partials/) since Hugo v0.146.0.
  • Dispatch a custom event or call autoInit directly after dynamic fragment swaps.
  • When available in your dependency set, use single plugin packages such as @preline/dropdown/non-auto when a Hugo section only needs one plugin.
  • Load optional third-party libraries before the first preline/non-auto import when using HSStaticMethods, or initialize late optional plugins with their direct classes. Datatable needs jquery + a DataTables build; Datepicker needs vanilla-calendar-pro and lodash (only the former is actually checked); File Upload needs lodash + dropzone; Range Slider needs only noUiSlider.

© 2026 Preline Labs.