Update v4.2 - New components, 10+ framework guides, and quality improvements. 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/_default/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.

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.

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.

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.
  • 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.

© 2026 Preline Labs.