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

Remix

Using Preline UI with Remix

A practical guide to wiring Preline UI into Remix without fighting server rendering, route transitions, and React lifecycle timing.

Remix has the same core integration rule as any server-rendered React app: let Remix render and hydrate the markup first, then let Preline UI attach behavior in the browser. The clean path is a small client effect that runs after route content exists in the DOM.

The current Preline UI package exposes module entries that fit that model: preline/non-auto for explicit scans, named plugin classes for manual instances, and single-plugin packages for smaller route surfaces.

Start with the Remix mental model

Preline UI is a DOM-driven Tailwind CSS component system. Remix renders React routes, can stream or server-render HTML, and then hydrates on the client. Preline UI should not be part of the server render path; it should read the browser DOM after React has committed it.

That keeps responsibilities clear. Remix owns routing, data loading, and rendering. Preline UI owns the JavaScript behavior around already-rendered dropdowns, overlays, tabs, selects, tooltips, and other marked-up components.

Keep Preline UI inside a client effect

Put page-level initialization in a component that renders in the Remix root layout and uses useEffect. The dynamic import keeps Preline UI out of server execution and gives Remix time to hydrate route markup before autoInit scans it.

app/components/PrelineClient.tsx
                        
                          import { useLocation } from "@remix-run/react";
                          import { useEffect } from "react";

                          export default function PrelineClient() {
                            const location = useLocation();

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

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

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

                            return null;
                          }
                        
                      

Render that component near the end of the document body, after <Outlet />, so route content has already been committed when the effect runs.

app/root.tsx
                        
                          import { Outlet, Scripts, ScrollRestoration } from "@remix-run/react";
                          import PrelineClient from "./components/PrelineClient";

                          export default function App() {
                            return (
                              <html lang="en">
                                <body>
                                  <Outlet />
                                  <PrelineClient />
                                  <ScrollRestoration />
                                  <Scripts />
                                </body>
                              </html>
                            );
                          }
                        
                      

Reinitialize after Remix navigation

Remix can replace route content during client-side navigation without reloading the page. Use useLocation() as the signal to scan the new DOM after the route changes.

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 a route only needs a few plugins, pass collection keys such as dropdown, overlay, select, tooltip, tabs, or range-slider.

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

Choose imports by lifecycle control

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

Client-only code
                        
                          const { HSStaticMethods } = await import("preline/non-auto");

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

Static named imports from the same entry are useful inside code that only runs on the client. In shared Remix modules, prefer the dynamic import inside useEffect so server execution never touches browser-only plugin logic.

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

Terminal
                        
                          npm install @preline/dropdown
                        
                      
DropdownClient.tsx
                        
                          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 Remix route components, /non-auto keeps initialization aligned with hydration and route timing.

Use manual instances for component-owned nodes

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

Dropdown.tsx
                        
                          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 Remix or React context and is part of why the same codebase works in plain HTML, React, Vue, Angular, Svelte, SolidJS, Next.js, Nuxt, and Remix.

In Remix, 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 Remix Preline client loader.

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

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 in Remix client effects.
  • Use preline/non-auto when you need explicit lifecycle control.
  • Run autoInit after Remix navigation with useLocation().
  • 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 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.