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

Express

Using Preline UI with Express

A practical guide to wiring Preline UI into server-rendered Express pages without exposing your whole node_modules directory.

Express usually serves HTML templates and static assets. That is a natural fit for Preline UI: your view engine renders the Tailwind CSS markup, and Preline UI attaches behavior in the browser after the page loads.

The current Preline UI package supports two practical Express paths: serve the browser script in dist/index.js for ordinary server-rendered pages, or bundle preline/non-auto when you want explicit initialization timing and a smaller public asset surface.

Start with the Express mental model

Preline UI is a DOM-driven Tailwind CSS component system. Express renders HTML with EJS, Pug, Handlebars, Nunjucks, or plain responses. Preline UI reads the final browser DOM and wires up components such as dropdowns, overlays, tabs, selects, and tooltips.

There is no framework lifecycle to synchronize with on a normal Express page. Load the script after the markup, or initialize explicitly from a small browser module.

Serve only the assets you need

Avoid exposing the entire node_modules directory from Express. Either copy the Preline UI distribution files into your public directory or mount only the Preline package under a narrow static path.

Terminal
                        
                          npm install preline
                          mkdir -p public/vendor/preline
                          cp -R node_modules/preline/dist public/vendor/preline/dist
                        
                      
app.js
                        
                          import express from "express";
                          import path from "node:path";

                          const app = express();

                          app.use("/assets", express.static(path.join(process.cwd(), "public")));
                        
                      

Use the static script for rendered pages

For standard Express pages, load dist/index.js near the end of your shared layout. This browser build initializes after the page load event.

views/layout.ejs
                        
                          <body>
                            <%- body %>

                            <script src="/assets/vendor/preline/dist/index.js"></script>
                          </body>
                        
                      

Use this when each navigation returns a complete HTML document. Express sends the markup, the browser loads Preline UI, and the plugin collections start fresh on the new page.

Bundle Preline UI for explicit timing

If your Express project already bundles browser assets with esbuild, Vite, Rollup, or Webpack, import preline/non-auto and decide when the scan runs.

assets/js/preline.ts
                        
                          import { HSStaticMethods } from "preline/non-auto";

                          const initPreline = () => {
                            HSStaticMethods.autoInit();
                          };

                          if (document.readyState === "loading") {
                            document.addEventListener("DOMContentLoaded", initPreline, { once: true });
                          } else {
                            initPreline();
                          }

                          document.addEventListener("preline:init", initPreline);
                        
                      
package.json
                        
                          {
                            "scripts": {
                              "build:js": "esbuild assets/js/preline.ts --bundle --format=esm --platform=browser --outfile=public/js/preline.js"
                            }
                          }
                        
                      

Choose imports by page scope

For a whole Express site bundle, preline/non-auto gives you HSStaticMethods and named plugin classes without automatic page-load behavior. For a copied static module, the equivalent browser entry is /assets/vendor/preline/dist/non-auto.mjs.

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

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

assets/js/dropdown.ts
                        
                          import HSDropdown from "@preline/dropdown/non-auto";

                          document.addEventListener("DOMContentLoaded", () => {
                            HSDropdown.autoInit();
                          });
                        
                      

Use manual instances for custom browser code

autoInit is enough for normal Express templates. Manual instances are useful when a custom browser script owns one plugin root and can destroy it before replacing that DOM.

assets/js/dropdown.ts
                        
                          import {
                            HSDropdown,
                            type IHTMLElementFloatingUI,
                          } from "preline/non-auto";

                          const root = document.querySelector<HTMLDivElement>(".hs-dropdown");

                          if (root) {
                            const dropdown = new HSDropdown(
                              root as unknown as IHTMLElementFloatingUI,
                            );

                            window.addEventListener("beforeunload", () => {
                              dropdown.destroy();
                            });
                          }
                        
                      

Handle dynamic fragments intentionally

A normal Express navigation reloads the document, so plugin collections start cleanly. Extra cleanup only matters if you use HTMX, Turbo, PJAX, websockets, or another browser script that injects and removes partial HTML without a page reload.

Fragment update
                        
                          document.dispatchEvent(new Event("preline:init"));

                          HSStaticMethods.cleanCollection("dropdown");
                          HSStaticMethods.autoInit("dropdown");
                        
                      

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 Express browser bundle.

assets/js/range-slider.ts
                        
                          import noUiSlider from "nouislider";

                          (
                            globalThis as typeof globalThis & {
                              noUiSlider: typeof noUiSlider;
                            }
                          ).noUiSlider = noUiSlider;

                          const { HSRangeSlider } = await import("preline/non-auto");

                          HSRangeSlider.autoInit();
                        
                      

The practical checklist

  • Use dist/index.js from a public Preline copy for simple server-rendered Express pages.
  • Do not expose the whole node_modules directory as a public static route.
  • Bundle preline/non-auto when you need explicit initialization timing.
  • Dispatch a custom event or call autoInit directly after dynamic fragment swaps.
  • When available in your dependency set, use single plugin packages such as @preline/dropdown/non-auto when a route group only needs one plugin.
  • 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.