Update v4.2 - New components, 10+ framework guides, and quality improvements. Visit Changelog

Qwik

Using Preline UI with Qwik

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.

Start with the Qwik mental model

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.

Initialize from a visible task

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.

PrelineClient.tsx
                        
                          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.

Reinitialize after Qwik City navigation

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.

src/routes/layout.tsx
                        
                          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.

Choose imports by lifecycle control

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.

Browser task
                        
                          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.

Single plugin packages keep small Qwik routes 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 Qwik route only needs one or two interactive primitives.

Terminal
                        
                          npm install @preline/dropdown
                        
                      
DropdownClient.tsx
                        
                          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.

Use manual instances for component-owned nodes

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.

Dropdown.tsx
                        
                          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.

Cleanup matters because Preline UI keeps registries

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.

Cleanup
                        
                          HSStaticMethods.cleanCollection("dropdown");
                          HSStaticMethods.cleanCollection(["dropdown", "overlay"]);
                        
                      

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 general Qwik Preline task.

RangeSliderClient.tsx
                        
                          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.

The practical checklist

  • Keep Preline UI initialization inside Qwik browser-visible work, usually useVisibleTask$.
  • Use preline/non-auto when you need explicit lifecycle control.
  • Track useLocation().url.pathname in Qwik City when route content can change without a full reload.
  • When available in your dependency set, use single plugin packages such as @preline/dropdown/non-auto when a route only needs one plugin.
  • Use manual instances and visible task cleanup for reusable components that own one plugin root.
  • 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.