Update v5.0 - Preline MCP, AI Prompts, Animated Icons and more. Visit Changelog

Django

Using Preline UI with Django

A practical guide to wiring Preline UI into Django across static files and a django-vite build step, including the htmx re-init hook for projects that use htmx.

Django and Preline UI fit together cleanly once you separate the two layers. Django is a batteries-included Python web framework that renders HTML on the server with the Django Template Language and serves CSS and JavaScript from static/ folders collected by python manage.py collectstatic. Preline UI is the client-side behavior layer that reads that HTML in the browser and wires up the interaction. The small script your static/ folder ships is what carries Preline UI across that server-to-client boundary.

This guide walks through the integration choices that matter in a real Django project: where to run autoInit when you serve Preline UI from static files, how the same idea maps to a django-vite build step, how to keep Preline UI working when htmx swaps the DOM without a reload, how to use Preline UI markup inside .html Django templates, and how to avoid stale plugin references as pages change.

Start with the Django mental model

Preline UI is a DOM-driven Tailwind CSS component system. Django produces the markup, and Preline UI attaches behavior to that markup once it exists in the browser. That is why the same dropdown, overlay, tabs, tooltip, or select markup can move between Django, plain HTML, React, Vue, Svelte, Symfony, Rails, and similar stacks.

The detail that matters in Django is where the JavaScript runs. Django templates render server-side HTML, so Preline UI cannot initialize from inside a .html template. It needs a client script that your static/ folder serves, either the prebuilt Preline UI bundle you copy into static/js/ or a Vite-built entrypoint loaded through django-vite. Django is a traditional multi-page app, so each navigation is a full HTTP request and the page reloads, which means a single page-load scan is enough. As soon as you add htmx for partial DOM updates, navigation becomes a partial swap and you have to re-run initialization. Everything below is about putting autoInit in the right place for each case.

Serve Preline UI from static files

Serving Preline UI from static/ is the simplest path, and it is the default for most Django projects because they do not run a JavaScript bundler. After npm install preline, copy the prebuilt bundle into a static directory Django collects. The full Django installation covers the CSS build step that brings in the Preline UI Tailwind CSS variants.

Terminal
                        
                          mkdir -p static/js
                          cp node_modules/preline/dist/preline.js static/js/
                        
                      

Reference the compiled Tailwind CSS and the Preline UI bundle from your base Django template. Load the static tag library once at the top of the template, then build each URL with {% static '...' %}. The bundled preline.js exposes the window.HSStaticMethods global and runs autoInit for you when the page loads, so a plain Django page needs no extra script.

templates/base.html
                        
                          {% load static %}

                          <link rel="stylesheet" href="{% static 'css/output.css' %}">

                          <!-- Before the closing body tag -->
                          <script src="{% static 'js/preline.js' %}"></script>
                        
                      

Because standard Django navigation is a full page load, the bundle re-runs and scans the fresh DOM on every page. autoInit also skips nodes that already have plugin instances, so re-running it is always safe. Keep the window.HSStaticMethods global in mind: the htmx section below uses it to re-initialize after a partial swap.

Use django-vite when you want a build step

If you prefer a bundler over copying files, run Vite alongside Django with the django-vite package, which renders the right asset tags for Vite's dev server and production manifest. With Vite you install Preline UI from npm and import it from a JavaScript entrypoint. Vite bundles that file, so module imports work as expected and preline/non-auto becomes a good default: nothing runs until your code decides the DOM is ready.

Terminal
                        
                          npm install preline
                        
                      
static_src/main.js
                        
                          import "preline/variants.css";
                          import "preline/theme.css";
                          import { HSStaticMethods } from "preline/non-auto";

                          function autoInit() {
                            HSStaticMethods.autoInit();
                          }

                          if (document.readyState === "loading") {
                            document.addEventListener("DOMContentLoaded", autoInit);
                          } else {
                            autoInit();
                          }
                        
                      

Load the entry from your base template with the django-vite tags. The rest of this guide applies the same way to both paths: the static bundle exposes window.HSStaticMethods, while Vite exposes the plugin classes through bundled module imports from preline/non-auto.

preline/variants.css and preline/theme.css are Tailwind CSS source partials, not a precompiled stylesheet, so importing them only works once Vite's Tailwind CSS plugin processes them alongside your own CSS entry. See the Django installation guide for the full Tailwind CSS setup.

templates/base.html
                        
                          {% load django_vite %}

                          <head>
                            {% vite_hmr_client %}
                            {% vite_asset 'static_src/main.js' %}
                          </head>
                        
                      

Reinitialize on htmx swaps

This is the key step for any Django project that uses htmx. htmx intercepts requests from hx-* attributes and replaces part of the page with the HTML fragment your Django view returns, instead of doing a full reload. Your initial page-load scan runs once, but new markup that htmx swaps in never gets initialized. This is the same re-init problem you see with Turbo in Symfony and Rails, and the fix is the same shape: re-run autoInit after each swap.

htmx fires htmx:afterSwap after it places the new content in the DOM. Listen for it and re-run autoInit. autoInit skips elements that already have plugin instances, so re-running it after every swap keeps the whole page covered without double-initializing.

templates/base.html
                        
                          <script src="{% static 'js/preline.js' %}"></script>

                          <script>
                            // Fires after htmx swaps a fragment into the DOM
                            document.body.addEventListener("htmx:afterSwap", () => {
                              window.HSStaticMethods.autoInit();
                            });
                          </script>
                        
                      

If you need the markup to settle before initializing, htmx:afterSettle is an alternative that fires once htmx has finished its swap transitions. Under the Vite path, do the same from your entrypoint with the imported HSStaticMethods instead of the global. When a swap removes a region of Preline UI markup, pair the re-init with the cleanup covered further down so the registry only holds live nodes.

Choose imports by the level of control you need

Under the Vite path, preline/non-auto is the best default for a full Preline UI installation. It gives you HSStaticMethods and the plugin classes without depending on plugin auto-entry page-load timing, which matters once htmx controls when the DOM changes.

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

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

If a page only needs the class for one plugin from the full package, import that class from preline/non-auto. This keeps the example aligned with the package's declared surface while still letting your code decide when to initialize.

static_src/dropdown.js
                        
                          import { HSDropdown } from "preline/non-auto";

                          HSDropdown.autoInit();
                        
                      

With the static bundle there is no bundler to tree-shake, so the copied preline.js exposes the same plugin classes and HSStaticMethods through window.HSStaticMethods instead.

Single plugin packages keep small Django surfaces focused

Not every Django app needs the whole library. 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 keeps the integration focused when only a small part of the page needs Preline UI behavior, and it pairs naturally with the Vite path.

Terminal
                        
                          npm install @preline/dropdown
                        
                      
static_src/main.js
                        
                          import HSDropdown from "@preline/dropdown/non-auto";

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

In that single-package setup, the auto entry is import "@preline/dropdown". Use it only for plain Django pages with full reloads. As soon as htmx swaps are involved, the /non-auto entry keeps initialization aligned with the timing you control.

The main preline package exports Advanced Datepicker styles as preline/datepicker-styles.css and preline/datepicker-styles-utility.css. Import the appropriate public path directly; install @preline/datepicker only when you intentionally want the standalone plugin package.

Use Preline UI markup in Django templates

Django templates only emit HTML, so Preline UI markup drops straight into a .html template with the same Tailwind CSS classes you would use anywhere else. The {% %} tags and {{ }} variables render before the response leaves the server, and the static bundle or Vite entry your base template loads is what attaches the behavior once the page reaches the browser.

templates/partials/actions_menu.html
                        
                          <div class="hs-dropdown relative inline-flex">
                            <button id="hs-dropdown-example" type="button" class="hs-dropdown-toggle ..." aria-haspopup="menu" aria-expanded="false" aria-label="Dropdown">
                              Actions
                            </button>

                            <div class="hs-dropdown-menu ... hidden" role="menu" aria-orientation="vertical" aria-labelledby="hs-dropdown-example">
                              ...
                            </div>
                          </div>
                        
                      

Make sure your base template loads the script that ships Preline UI, the copied preline.js or your Vite-built bundle, so every page that renders Preline UI markup also ships the script that initializes it. When you return the same markup from an htmx fragment view, the htmx:afterSwap re-init from above is what wires up the freshly swapped subtree.

Use manual instances when you own a specific node

autoInit is good for page-level markup. A manual instance is better when one specific node needs explicit control. Create the instance from your entry and guard it with getInstance so a re-run does not double-initialize the same node.

static_src/main.js
                        
                          import { HSDropdown } from "preline/non-auto";

                          function initUserMenu() {
                            const el = document.querySelector("#user-menu");
                            if (el && !HSDropdown.getInstance(el)) {
                              new HSDropdown(el);
                            }
                          }

                          document.addEventListener("DOMContentLoaded", initUserMenu);
                        
                      

The getInstance guard keeps a single instance per node. If htmx is allowed to swap the node out, prefer page-level autoInit on htmx:afterSwap over a long-lived manual instance, since the element it points at can be replaced. With the static bundle, reach the same plugin class through window.HSStaticMethods instead of the module import.

Cleanup matters because Preline UI keeps registries

Preline UI stores plugin instances in internal collections such as window.$hsDropdownCollection. That registry lets plugins coordinate in plain HTML, Django, and other environments without framework context.

A full page load resets the registry, so plain Django navigation needs nothing extra. An htmx swap replaces part of the DOM without a reload, so a registry entry can linger as a stale reference when its markup is swapped out. When a swap removes a region of Preline UI markup, clean the relevant collection before re-running autoInit so the registry only holds live nodes.

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

Optional dependencies: scope them per plugin, or load them all with the static bundle

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, and Datepicker only needs vanilla-calendar-pro. Whether a missing dependency stays isolated to its own plugin depends on which install path you're on. With a single-plugin package (@preline/datatable, @preline/datepicker, and so on) each plugin ships its own isolated bundle, so scoping the optional dependency to only the pages that use that plugin is safe.

The static bundle (preline.js, covered above) is different: loading it at all runs HSStaticMethods's aggregator, which eagerly requires every plugin's implementation module up front to build the shared registry, not just the plugins your current page's markup uses. Most plugin modules guard their optional dependency with a runtime check that safely no-ops when it's missing, but not every plugin does. Datepicker's implementation extends VanillaCalendarPro.Calendar unconditionally at module-evaluation time, so if vanilla-calendar-pro isn't already on window when preline.js runs, that throws during the bundle's own startup. Because the aggregator requires plugins in a fixed sequence, an uncaught error there can stop whichever plugins come after it in that sequence from registering too, even on a page with no datepicker markup at all. If you're on the static bundle path, load every optional dependency your project uses (jQuery, datatables.net, lodash, Dropzone, noUiSlider, vanilla-calendar-pro) globally, before preline.js, on every page, rather than scoping them per page.

Range Slider uses the JavaScript API from noUiSlider. Preline UI remains responsible for the Tailwind CSS markup and behavior wrapper, so you do not need to bring in noUiSlider CSS just to make Preline UI styling work.

The practical checklist

  • Initialize Preline UI from a script your static/ folder serves, not from a .html Django template.
  • Serving the copied preline.js bundle is the simplest path and exposes window.HSStaticMethods; with django-vite, import from preline/non-auto and run HSStaticMethods.autoInit() once the DOM is ready.
  • Load the static tag library with {% load static %} and reference assets with {% static '...' %} so every page that renders Preline UI markup also ships the script that initializes it.
  • If the project uses htmx, run autoInit on htmx:afterSwap (or htmx:afterSettle) so swapped-in markup gets initialized.
  • Guard manual instances with getInstance, and call cleanCollection when a swap removes a region of initialized markup.
  • With single-plugin packages, install optional third-party dependencies only for the plugin that needs them, such as datatables.net for Datatable or noUiSlider for Range Slider. With the static preline.js bundle, load all of them globally on every page instead, since the bundle initializes every plugin's module on load regardless of which page you're on, and a plugin missing its dependency can prevent unrelated plugins from registering.

© 2026 Preline Labs.