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

Next.js

Using Preline UI with Next.js

A practical guide to wiring Preline UI into Next.js without fighting Server Components, client navigation, and strict TypeScript.

Next.js adds one extra rule to the usual React integration: Preline UI must stay on the client side. React and Server Components can produce the markup, but Preline UI should only scan and wire that markup after it exists in the browser.

The current Preline UI package exposes browser-side module entries that fit App Router boundaries: preline/non-auto for explicit scans, named plugin classes for manual instances, and single-plugin packages for smaller client surfaces.

Start with the Next.js mental model

Preline UI is a DOM-driven Tailwind CSS component system. Next.js renders React trees, often across a Server Component and Client Component boundary. Preline UI reads the browser DOM and attaches behavior to matching markup after hydration.

That means the integration should stay inside a client boundary. Let Next.js render and hydrate first, then initialize Preline UI from a client component.

Keep the loader behind a client boundary

Create a tiny client component for Preline UI initialization. Dynamic import keeps the module browser-only and avoids touching the DOM during the server render.

app/components/PrelineClient.tsx
                        
                          "use client";

                          import { usePathname } from "next/navigation";
                          import { useEffect } from "react";

                          export default function PrelineClient() {
                            const pathname = usePathname();

                            useEffect(() => {
                              let cancelled = false;

                              import("preline/non-auto").then(({ HSStaticMethods }) => {
                                if (!cancelled) HSStaticMethods.autoInit();
                              });

                              return () => {
                                cancelled = true;
                              };
                            }, [pathname]);

                            return null;
                          }
                        
                      

Add it near the end of the App Router layout body so route content is already part of the committed React tree when the effect runs.

app/layout.tsx
                        
                          import PrelineClient from "./components/PrelineClient";

                          export default function RootLayout({
                            children,
                          }: {
                            children: React.ReactNode;
                          }) {
                            return (
                              <html lang="en">
                                <body>
                                  {children}
                                  <PrelineClient />
                                </body>
                              </html>
                            );
                          }
                        
                      

Reinitialize after App Router navigation

App Router navigation can replace route markup without reloading the page. Use usePathname() as the dependency that tells the Preline UI loader to scan the new DOM.

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.

If you are still using the Pages Router, use the same idea with router.asPath from next/router as the effect dependency.

Choose imports by the level of control you need

For a full Preline UI installation, preline/non-auto is the best Next.js default. It gives you HSStaticMethods and named plugin classes without automatic page-load initialization.

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

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

Inside a client component, named imports from the same entry are also useful for manual instances.

Client component
                        
                          import { HSDropdown, HSStaticMethods } from "preline/non-auto";
                        
                      

Single plugin packages keep small Next.js surfaces 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 route segment only needs one or two interactive primitives.

Terminal
                        
                          npm install @preline/dropdown
                        
                      
DropdownClient.tsx
                        
                          "use client";

                          import { useEffect } from "react";
                          import HSDropdown from "@preline/dropdown/non-auto";

                          export default function DropdownClient() {
                            useEffect(() => {
                              HSDropdown.autoInit();
                            }, []);

                            return (
                              <div className="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 Next.js client components, /non-auto keeps initialization aligned with hydration and navigation timing.

Use manual instances for component-owned nodes

autoInit is good for page-level scans. A manual instance is better when one client component owns one plugin root and can destroy it directly.

Dropdown.tsx
                        
                          "use client";

                          import { useEffect, useRef } from "react";
                          import {
                            HSDropdown,
                            type IHTMLElementFloatingUI,
                          } from "preline/non-auto";

                          export default function Dropdown() {
                            const dropdownRef = useRef<HTMLDivElement>(null);

                            useEffect(() => {
                              if (!dropdownRef.current) return;

                              const dropdown = new HSDropdown(
                                dropdownRef.current as unknown as IHTMLElementFloatingUI,
                              );

                              return () => {
                                dropdown.destroy();
                              };
                            }, []);

                            return (
                              <div ref={dropdownRef} className="hs-dropdown relative inline-flex">
                                <button className="hs-dropdown-toggle" type="button">
                                  Toggle
                                </button>
                                <div className="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 a React context and is part of why the same codebase works in plain HTML, React, Vue, Svelte, Angular, SolidJS, and Next.js.

In Next.js, call destroy() 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 Preline client loader.

RangeSliderClient.tsx
                        
                          "use client";

                          import { useEffect } from "react";
                          import noUiSlider from "nouislider";

                          export default function RangeSliderClient() {
                            useEffect(() => {
                              (
                                globalThis as typeof globalThis & {
                                  noUiSlider: typeof noUiSlider;
                                }
                              ).noUiSlider = noUiSlider;

                              import("preline/non-auto").then(({ HSRangeSlider }) => {
                                HSRangeSlider.autoInit();
                              });
                            }, []);

                            return null;
                          }
                        
                      

Render the Range Slider markup in the route or component that owns it. 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 client components.
  • Use preline/non-auto when you need route-aware lifecycle control.
  • Run autoInit after App Router navigation with usePathname().
  • 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 destroy() for reusable client 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.