Update v5.0 - Preline MCP, AI Prompts, Animated Icons and more. 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/preline.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. Copy only the specific browser files you actually reference, not the whole dist directory, which also contains per-plugin bundles, sourcemaps, and type declarations you will not serve.

Terminal
                        
                          npm install preline
                          mkdir -p public/javascripts
                          cp node_modules/preline/dist/preline.js public/javascripts/preline.js
                          cp node_modules/preline/dist/non-auto.mjs public/javascripts/non-auto.mjs
                        
                      
app.js
                        
                          const express = require("express");
                          const path = require("path");

                          const app = express();

                          app.use(express.static(path.join(__dirname, "public")));
                        
                      

Use the static script for rendered pages

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

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

                            <script src="/javascripts/preline.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.

Load a theme

Point an explicit @source at your own view templates too. Tailwind CSS v4's automatic content detection already picks up most template files, but declaring the glob yourself keeps the build reproducible if that detection ever misses a file, for example when the templates directory is .gitignored or the CLI runs from an unexpected working directory.

Import preline/theme.css directly from the package to load Preline's default theme. Tailwind CSS resolves this bare specifier from node_modules at build time, the same way it resolves the @source path above, so there is nothing extra to wire up in Express.

styles/tailwind.css
                        
                          @import "tailwindcss";

                          /* Your view templates (adjust the glob to match your view engine) */
                          @source "../views/**/*.ejs";

                          @source "../node_modules/preline/dist/*.js";
                          @import "preline/variants.css";
                          @import "preline/theme.css";
                        
                      

To use a different look, either swap in one of Preline's built-in preset themes (see the Theme docs), or generate a custom brand theme with the bundled generator. The generator resolves one of a small allowlist of conventional theme directories in your own project and writes a single <theme-name>.css file there. It is not a file the package ships for you to edit in place.

Terminal
                        
                          node node_modules/preline/skills/theme-generator/scripts/run-theme-generator.js \
                            --name brand \
                            --primary-color "#2F6BFF" \
                            --output styles/themes/brand.css
                        
                      

Import the generated file with a relative path instead of the default theme import, and drop the preline/theme.css line:

styles/tailwind.css
                        
                          @import "tailwindcss";
                          @source "../node_modules/preline/dist/*.js";
                          @import "preline/variants.css";

                          @import "./themes/brand.css";
                        
                      

The generated file lives next to styles/tailwind.css, not under public/. It is a source file that Tailwind CSS compiles in, not an asset Express serves directly, so no extra Express wiring is needed.

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 /javascripts/non-auto.mjs.

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

Import a single plugin to keep small routes focused

There is a single preline package, not per-plugin packages. Each plugin is still reachable on its own through the package's ./plugins/* subpath export, for example preline/plugins/dropdown, preline/plugins/overlay, preline/plugins/select, or preline/plugins/range-slider. This is useful when one Express route group only needs one or two interactive primitives and pulling in the full bundle would be wasteful.

assets/js/dropdown.ts
                        
                          import HSDropdown from "preline/plugins/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.

Four plugins gate themselves behind third-party globals, and each checks for its dependency once, when the plugin module itself parses, rather than lazily when a matching element first appears in the DOM. If the dependency is missing at that moment, the plugin silently no-ops for the rest of the page; reloading the library afterward and calling autoInit() again does not recover it.

  • Datatable needs jQuery and datatables.net’s own DataTable global: two separate globals, not one. datatables.net registers itself as a jQuery plugin, but the Preline plugin also references the bare DataTable constructor directly.
  • Datepicker needs lodash (the bare _ global) and vanilla-calendar-pro.
  • File Upload needs lodash and Dropzone; the lodash dependency is easy to miss since it is not implied by the plugin name.
  • Range Slider needs only noUiSlider.

These dependencies do not need to be loaded for dropdowns, overlays, tabs, tooltips, or any other plugin that does not appear in the list above.

If you initialize optional plugins through HSStaticMethods, make every 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();
                        
                      

Datatable needs the same treatment, but with both of its globals set first:

assets/js/datatable.ts
                        
                          import jQuery from "jquery";
                          import DataTable from "datatables.net";

                          const g = globalThis as typeof globalThis & {
                            jQuery: typeof jQuery;
                            DataTable: typeof DataTable;
                          };

                          g.jQuery = jQuery;
                          g.DataTable = DataTable;

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

                          HSDataTable.autoInit();
                        
                      

The practical checklist

  • Use dist/preline.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 a route group only needs one plugin, import it directly through a subpath export such as preline/plugins/dropdown-non-auto instead of the full bundle.
  • 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.