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

Astro

Using Preline UI with Astro

A practical guide to wiring Preline UI into Astro without fighting its server-first, islands architecture.

Astro and Preline UI fit together cleanly once you separate the two layers. Astro renders HTML on the server and ships zero JavaScript by default. Preline UI is the client-side behavior layer that reads that HTML in the browser and wires up the interaction. The small amount of script you add is what carries Preline UI across that server-to-client boundary.

This guide walks through the integration choices that matter in a real Astro project: where to run autoInit, how astro:page-load keeps things working with View Transitions, how to handle Preline UI markup that lives inside framework islands, and how to avoid stale plugin references as Astro swaps pages.

Start with the Astro mental model

Preline UI is a DOM-driven Tailwind CSS component system. Astro 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 Astro, plain HTML, React, Vue, Svelte, Laravel, Rails, and similar stacks.

The detail that matters in Astro is where the JavaScript runs. A .astro file is server-rendered static markup, so Preline UI cannot initialize from inside its frontmatter. It needs a client <script> that Astro bundles and ships, or a framework island that hydrates in the browser. Everything below is about putting initialization in the right place for each case.

Load Preline UI from a layout client script

Put initialization in a client <script> inside your shared layout so every page that uses it gets Preline UI behavior. Astro processes and bundles that script, so module imports work as expected. preline/non-auto is a good default here: nothing runs until the script decides the DOM is ready.

Layout.astro
                        
                          ---
                          import "../styles/global.css";
                          ---

                          <slot />

                          <script>
                            import { HSStaticMethods } from "preline/non-auto";

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

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

                            document.addEventListener("astro:page-load", init);
                          </script>
                        
                      

The astro:page-load listener is what keeps Preline UI working with View Transitions. When the <ClientRouter /> swaps page content without a full reload, that event fires and re-scans the new DOM. autoInit skips nodes that already have plugin instances, so running it again on every navigation is safe.

The main entry, import "preline", registers its own window load listener and initializes from there. That works for a plain page, but it is easy to miss in a bundler flow: if your script awaits anything before importing Preline UI (dynamic imports of optional dependencies, for example), the listener can be registered after load has already fired, and nothing initializes. The /non-auto entry avoids that timing question entirely, since initialization only happens where you call autoInit().

Initialize Preline UI inside framework islands

A framework island rendered with a client:* directive hydrates on its own schedule, which can be later than the layout script. If Preline UI markup lives inside that island, a single layout scan may run before the island exists. Initialize inside the island using its own framework lifecycle instead, exactly as you would in a standalone React, Vue, or Svelte app.

DropdownIsland.tsx
                        
                          import { useEffect } from "react";
                          import { HSStaticMethods } from "preline/non-auto";

                          export default function DropdownIsland() {
                            useEffect(() => {
                              HSStaticMethods.autoInit(["dropdown"]);
                            }, []);

                            return (
                              <div className="hs-dropdown relative inline-flex">
                                ...
                              </div>
                            );
                          }
                        
                      

Use the island in a page with the directive that matches when the behavior should become interactive, for example <DropdownIsland client:load /> or <DropdownIsland client:visible />. Keep the layout script for static .astro markup, and let islands own the timing for the markup they render.

Choose imports by the level of control you need

For a full Preline UI installation, preline/non-auto is the best Astro default. It gives you HSStaticMethods and the plugin classes, and leaves the initialization moment to you instead of tying it to the load event the main entry listens for. That matters most when your script awaits other work first, which is common once optional dependencies are involved.

preline.ts
                        
                          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 TypeScript surface while still letting your code decide when to initialize.

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

                          HSDropdown.autoInit();
                        
                      

Single plugin packages keep small Astro surfaces focused

Not every Astro site 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.

Terminal
                        
                          npm install @preline/dropdown
                        
                      
Layout.astro
                        
                          <script>
                            import HSDropdown from "@preline/dropdown/non-auto";

                            document.addEventListener("astro:page-load", () => {
                              HSDropdown.autoInit();
                            });
                          </script>
                        
                      

In that single-package setup, the auto entry is import "@preline/dropdown". Use it for simple static pages. When Astro client navigation or framework islands are involved, the /non-auto entry keeps initialization aligned with the timing you control.

Use manual instances when an island owns the DOM node

autoInit is good for page-level markup. Manual instances are better when a framework island owns one specific plugin root and can clean it up directly. This is especially useful for reusable island components and conditionally rendered overlays.

DropdownIsland.tsx
                        
                          import { useEffect, useRef } from "react";
                          import { HSDropdown, type IHTMLElementFloatingUI } from "preline/non-auto";

                          export default function DropdownIsland() {
                            const ref = useRef<HTMLDivElement | null>(null);

                            useEffect(() => {
                              if (!ref.current) return;

                              const dropdown = new HSDropdown(ref.current as unknown as IHTMLElementFloatingUI);

                              return () => {
                                dropdown.destroy();
                              };
                            }, []);

                            return (
                              <div ref={ref} className="hs-dropdown relative inline-flex">
                                ...
                              </div>
                            );
                          }
                        
                      

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, Astro, and other environments without framework context.

In Astro, most pages can rely on layout-level autoInit, which filters stale collection entries on each scan. When an island creates a manual instance, destroy it on unmount. When you intentionally remove a whole group of initialized markup, clean the relevant collection.

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. If jQuery and DataTables are not present, Datatable should not initialize. That dependency does not affect dropdowns, overlays, tabs, or tooltips.

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 dependency check for Datatable, Datepicker, Range Slider, and File Upload runs once, when Preline UI's module is evaluated — not when autoInit is called later. In the ES module build that Astro and Vite resolve for both import "preline" and import { HSStaticMethods } from "preline/non-auto", that evaluation is fully resolved before any of your own script's top-level code runs, including a window.DataTable = ... assignment placed above it. If a plugin's dependency is not on window at that exact moment, the plugin is excluded for the rest of that page load, and calling autoInit() again after loading the dependency will not recover it.

Import Preline UI dynamically instead, after assigning the dependency to window, the way the installation guide's loader does:

preline.ts
                        
                          const dtModule = await import("datatables.net-dt");
                          window.DataTable = dtModule.default ?? dtModule;

                          // Only now is it safe to load Preline UI itself
                          const { HSStaticMethods } = await import("preline/non-auto");
                          HSStaticMethods.autoInit();
                        
                      

The practical checklist

  • Initialize Preline UI from a client <script> in your layout, not from .astro frontmatter.
  • Use preline/non-auto and run HSStaticMethods.autoInit() on load and on astro:page-load so View Transitions keep working.
  • For Preline UI markup inside a framework island, initialize with the island's own lifecycle instead of relying on the layout scan.
  • Create manual plugin instances when an island owns one specific node, and call destroy() on unmount.
  • Install optional third-party dependencies only for the plugins that need them, such as datatables.net-dt for Datatable or noUiSlider for Range Slider.
  • Assign those dependencies to window before importing Preline UI, and load Preline UI itself with a dynamic await import("preline/non-auto") so the assignment always runs first — the dependency check happens once, when Preline UI's module evaluates.

© 2026 Preline Labs.