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 Qwik without fighting resumability, Qwik City navigation, and browser-only DOM timing.
Qwik changes the usual client-side mental model because it resumes server-rendered HTML instead of eagerly hydrating the whole page. Preline UI still fits that model, but it should be initialized only when the relevant DOM is visible in the browser.
The current Preline UI package exposes module entries that work well with Qwik's browser boundary: preline/non-auto for explicit scans, named plugin classes for manual instances, and single-plugin packages for smaller route surfaces.
Preline UI is a DOM-driven Tailwind CSS component system. Qwik renders HTML, serializes state, and resumes only the pieces of JavaScript that become necessary. Preline UI should therefore live in the browser-only part of a Qwik component, after the element it needs to scan already exists.
That keeps the split clean. Qwik owns rendering and resumability. Preline UI owns the JavaScript behavior around already-rendered dropdowns, overlays, tabs, selects, tooltips, and other marked-up components.
Qwik's useVisibleTask$ is the practical place to initialize a browser DOM library. Use a dynamic import so preline/non-auto is loaded when the browser-side task runs, not as part of server rendering.
import { component$, useVisibleTask$ } from "@builder.io/qwik";
export default component$(() => {
useVisibleTask$(async () => {
const { HSStaticMethods } = await import("preline/non-auto");
HSStaticMethods.autoInit();
});
return null;
});
Use this pattern deliberately. A visible task wakes JavaScript for that component, which is exactly what you want for third-party DOM behavior, but it should stay close to the route or component that actually contains Preline UI markup.
Qwik City can replace route content without a full page reload. Track location.url.pathname so the visible task reruns when new route markup appears.
import { component$, Slot, useVisibleTask$ } from "@builder.io/qwik";
import { useLocation } from "@builder.io/qwik-city";
export default component$(() => {
const location = useLocation();
useVisibleTask$(async ({ track }) => {
track(() => location.url.pathname);
const { HSStaticMethods } = await import("preline/non-auto");
HSStaticMethods.autoInit();
});
return <Slot />;
});
autoInit is collection-aware. It filters stale nodes that are no longer in the document and skips elements that already have plugin instances, so repeated route-level scans are expected.
For a full Preline UI installation, preline/non-auto is the practical Qwik default. It gives you HSStaticMethods and named plugin classes without relying on automatic page-load initialization.
const { HSStaticMethods } = await import("preline/non-auto");
HSStaticMethods.autoInit(["dropdown", "overlay"]);
HSStaticMethods.cleanCollection(["dropdown", "overlay"]);
Static named imports can be useful in code that you know is browser-only, but dynamic imports inside useVisibleTask$ are easier to reason about in Qwik because the server path never evaluates Preline UI plugin code.
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 Qwik route only needs one or two interactive primitives.
npm install @preline/dropdown
import { component$, useVisibleTask$ } from "@builder.io/qwik";
export default component$(() => {
useVisibleTask$(async () => {
const { default: HSDropdown } = await import("@preline/dropdown/non-auto");
HSDropdown.autoInit();
});
return (
<div class="hs-dropdown relative inline-flex">
...
</div>
);
});
In that single-package setup, the auto entry is import "@preline/dropdown". It is useful for simple static pages. In Qwik components, /non-auto keeps initialization tied to the visible task that owns the DOM timing.
autoInit is good for route-level scans. A manual instance is better when one Qwik component owns one plugin root and can destroy it through the visible task cleanup.
import {
component$,
useSignal,
useVisibleTask$,
} from "@builder.io/qwik";
import type { IHTMLElementFloatingUI } from "preline/non-auto";
export default component$(() => {
const dropdownRef = useSignal<HTMLDivElement>();
useVisibleTask$(async ({ cleanup }) => {
const root = dropdownRef.value;
if (!root) return;
const { HSDropdown } = await import("preline/non-auto");
const dropdown = new HSDropdown(
root as unknown as IHTMLElementFloatingUI,
);
cleanup(() => dropdown.destroy());
});
return (
<div ref={dropdownRef} class="hs-dropdown relative inline-flex">
<button class="hs-dropdown-toggle" type="button">
Toggle
</button>
<div class="hs-dropdown-menu hidden">Menu</div>
</div>
);
});
The cast is only for strict TypeScript. At runtime the plugin receives the actual dropdown root element and augments it with Floating UI internals.
Preline UI stores plugin instances in internal collections such as window.$hsDropdownCollection. That registry lets plugins coordinate without Qwik context and is part of why the same codebase works in plain HTML, React, Vue, Angular, Svelte, SolidJS, Next.js, Nuxt, Remix, and Qwik.
In Qwik, call destroy() from the visible task cleanup for manual instances. For route-level scans, autoInit already filters removed nodes for the plugin collections it scans.
HSStaticMethods.cleanCollection("dropdown");
HSStaticMethods.cleanCollection(["dropdown", "overlay"]);
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 general Qwik Preline task.
import { component$, useVisibleTask$ } from "@builder.io/qwik";
export default component$(() => {
useVisibleTask$(async () => {
const { default: noUiSlider } = await import("nouislider");
(
globalThis as typeof globalThis & {
noUiSlider: typeof noUiSlider;
}
).noUiSlider = noUiSlider;
const { HSRangeSlider } = await import("preline/non-auto");
HSRangeSlider.autoInit();
});
return null;
});
For a late-loaded optional plugin, use the direct plugin class like HSRangeSlider. If you want to use HSStaticMethods.autoInit(["range-slider"]) instead, expose noUiSlider in the same first loader before any other preline/non-auto import happens.
useVisibleTask$.preline/non-auto when you need explicit lifecycle control.useLocation().url.pathname in Qwik City when route content can change without a full reload.@preline/dropdown/non-auto when a route 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 SolidJS projects, including onMount, createEffect, router rescans, module imports, cleanup, and optional dependencies.
View guide