Using Preline UI with React + Vite
A practical guide to using Preline UI JavaScript plugins in React + Vite projects, including autoInit, module imports, 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 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.
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.
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.
"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.
import PrelineClient from "./components/PrelineClient";
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<body>
{children}
<PrelineClient />
</body>
</html>
);
}
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.
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.
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.
import { HSDropdown, HSStaticMethods } from "preline/non-auto";
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.
npm install @preline/dropdown
"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.
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.
"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.
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.
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 Preline client loader.
"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.
preline/non-auto when you need route-aware lifecycle control.autoInit after App Router navigation with usePathname().@preline/dropdown/non-auto when a route only needs one plugin.destroy() for reusable client components that own one plugin root.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 React + Vite projects, including autoInit, module imports, cleanup, and optional dependencies.
View guideA practical guide to using Preline UI JavaScript plugins in Remix projects, including client effects, route rescans, module imports, cleanup, and optional dependencies.
View guide