Using Preline UI with Vue
A practical guide to using Preline UI JavaScript plugins in Vue projects, including nextTick, autoInit, module imports, cleanup, and optional dependencies.
View guideUpdate v5.0 - Preline MCP, AI Prompts, Animated Icons and more. Visit Changelog
A practical guide to wiring Preline UI into Svelte without fighting Svelte rendering, actions, and SvelteKit navigation.
Svelte and Preline UI work cleanly when Svelte owns rendering and Preline UI runs after the DOM exists. The integration is mostly about timing: initialize after Svelte has flushed the markup, and destroy manually created plugin instances when Svelte removes the node.
The current Preline UI package exposes the pieces Svelte needs directly: preline/non-auto for explicit scans, named plugin classes for manual instances, the preline/plugins/<name> subpath for a smaller surface within the same dependency, and standalone @preline/<name> packages when you don't want preline as a dependency at all.
Preline UI is a DOM-driven Tailwind CSS component system. Svelte compiles and updates the DOM; Preline UI scans that DOM and attaches behavior to matching markup. This is why the same dropdown, overlay, tabs, tooltip, or select markup can move between Svelte, React, Vue, Angular, plain HTML, Astro, Laravel, Rails, and similar stacks.
The tradeoff is explicit lifecycle work. Preline UI should run after Svelte renders, not while the component is still being created on the server or before the route content is present.
If your app already has a root layout running autoInit on every route (the next section), most components never need their own onMount call, since the layout-level scan picks up any markup a component renders. Reach for a component-local onMount only when a component can mount without a route change in between, for example content revealed by a boolean toggle rather than navigation. Use tick to wait for Svelte's DOM flush first, and keep the onMount callback synchronous if you return cleanup, because Svelte does not call a cleanup function returned from an async callback.
<script lang="ts">
import { onMount, tick } from "svelte";
import { HSStaticMethods } from "preline/non-auto";
onMount(() => {
tick().then(() => {
HSStaticMethods.autoInit(["dropdown"]);
});
});
</script>
Don't return HSStaticMethods.cleanCollection("dropdown") as the cleanup here: cleanCollection resets the tracking array for every dropdown on the page, not just this component's, so it would silently drop live instances that other components still own. It also never calls an instance's own destroy(), so it wouldn't remove this component's event listeners either. If this component can unmount without a page navigation and you need its instance gone immediately, create it directly and destroy that specific instance, as shown in the actions section below.
In SvelteKit, client-side navigation replaces route markup without a full page load, so a scan that only runs once will never see any page reached by clicking a link. afterNavigate (from $app/navigation) is the right hook for this: it fires once on the initial load and again on every later navigation, so a single call in the root layout covers both. Import your optional-dependency module before preline/non-auto, then reset and rescan.
<script lang="ts">
import { afterNavigate } from "$app/navigation";
let { children } = $props();
afterNavigate(async () => {
await import("$lib/vendor-globals");
const { HSStaticMethods } = await import("preline/non-auto");
HSStaticMethods.cleanCollection();
HSStaticMethods.autoInit();
});
</script>
{@render children()}
Two things worth knowing before you rely on this in production, both found by exercising an app built this way rather than by reading the code alone:
Fast repeated navigation can resolve out of order. The dynamic imports above are usually served from the module cache after the first navigation, but they're still asynchronous, so if a user navigates twice in quick succession, the first call's await can resolve after the second one already finished, and its cleanCollection/autoInit would run last and reset whatever the user is now looking at. Guard against it with a token that only the most recent call recognizes as current:
let navigationToken = 0;
afterNavigate(async () => {
const token = ++navigationToken;
try {
await import("$lib/vendor-globals");
const { HSStaticMethods } = await import("preline/non-auto");
if (token !== navigationToken) return;
HSStaticMethods.cleanCollection();
HSStaticMethods.autoInit();
} catch (error) {
console.error("Failed to initialize Preline UI", error);
}
});
A plugin that renders DOM outside the route content needs its own explicit cleanup. File Upload is the clearest example: Dropzone appends its hidden <input type="file"> to document.body, not inside the page markup SvelteKit's router actually swaps, so navigating away never removes it on its own, and cleanCollection only clears the tracking array; it never calls an instance's destroy(). Destroy any tracked File Upload instances at the top of the same callback, before the reset:
afterNavigate(async () => {
const token = ++navigationToken;
window.$hsFileUploadCollection?.forEach(({ element }) => element.destroy());
try {
/* ... */
}
});
Every plugin keeps its own window.$hs<PluginName>Collection array, so check whether a plugin you're using does something similar (appends to document.body, a portal, or anything else outside the element Svelte itself controls) before assuming a route-level rescan alone is enough.
For a full Preline UI installation, preline/non-auto is the best Svelte default. It gives you HSStaticMethods and plugin classes without depending on browser script or plugin auto-entry page-load timing.
import { HSStaticMethods } from "preline/non-auto";
HSStaticMethods.cleanCollection(["dropdown", "overlay"]);
HSStaticMethods.autoInit(["dropdown", "overlay"]);
If you want 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.
import { HSDropdown } from "preline/non-auto";
HSDropdown.autoInit();
If a Svelte surface genuinely only needs one plugin and pulling in the rest of the package's type surface through preline/non-auto feels like more than you need, and you're already depending on the full preline package elsewhere, import that plugin directly from its own subpath instead. No separate install, same version as the rest of your Preline UI dependency:
import HSDropdown from "preline/plugins/dropdown";
HSDropdown.autoInit();
Every Preline UI plugin also ships as its own independently versioned package under the @preline scope, for example @preline/dropdown, @preline/overlay, @preline/select, or @preline/range-slider. Unlike the preline/plugins/<name> subpath above, this is a genuinely separate install with its own CSS files. Reach for it when a Svelte surface (a standalone widget, a component library, a micro-frontend) needs one or two plugins and you don't want the full preline package as a dependency at all.
npm install @preline/dropdown
Because the CSS doesn't come bundled with a shared preline/theme.css/preline/variants.css import in this setup, import the plugin's own copies alongside its @source registration:
@import "tailwindcss";
@source "../../node_modules/@preline/dropdown/*.js";
@import "@preline/dropdown/theme.css";
@import "@preline/dropdown/variants.css";
<script lang="ts">
import { onMount, tick } from "svelte";
import HSDropdown from "@preline/dropdown/non-auto";
onMount(() => {
tick().then(() => {
HSDropdown.autoInit();
});
});
</script>
The auto entry, import "@preline/dropdown", self-initializes on the window load event, which makes it a fit for a plain static page but not for a SvelteKit route (it would never rerun after client-side navigation). @preline/dropdown/non-auto is the manual entry and keeps the same constructor and static-method surface as preline/non-auto's HSDropdown, just as a default export instead of a named one. Plugins with an optional third-party dependency (@preline/datatable, @preline/datepicker, @preline/range-slider, @preline/file-upload) still expect that dependency as a bare global exactly like their preline/plugins/<name> counterparts do. Installing the single-plugin package doesn't add jquery, dropzone, or the others as an npm dependency for you.
A Svelte action is a natural fit when one element owns one Preline UI plugin instance. The action receives the DOM node, creates the plugin instance, and returns a destroy hook that Svelte calls when the node is removed.
<script lang="ts">
import { HSDropdown, type IHTMLElementFloatingUI } from "preline/non-auto";
function prelineDropdown(node: HTMLElement) {
const dropdown = new HSDropdown(node as unknown as IHTMLElementFloatingUI);
return {
destroy() {
dropdown.destroy();
},
};
}
</script>
<div use:prelineDropdown class="hs-dropdown relative inline-flex">
...
</div>
Preline UI stores plugin instances in internal collections such as window.$hsDropdownCollection. That registry lets plugins coordinate in plain HTML, Svelte, and other environments without framework context.
In Svelte, actions should destroy their own manually created instance directly, as shown above. A route-level afterNavigate scan can call cleanCollection() as a blanket reset before every rescan without conflicting with that: it only clears the tracking array, so it never touches an instance an action already destroyed.
HSStaticMethods.cleanCollection("dropdown");
HSStaticMethods.cleanCollection(["dropdown", "overlay"]);
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-dt 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; noUiSlider's own CSS classes do not need to be merged into Preline UI styling.
Datepicker uses vanilla-calendar-pro for the calendar itself, and separately expects lodash on window, which is easy to miss since Lodash isn't in the plugin's name. File Upload uses dropzone for drag-and-drop handling and progress events; see the previous section for why File Upload specifically needs the extra cleanup step described there.
Each of these plugins expects its dependency as a bare global (Datatable's source declares declare var DataTable: any; and reads it off window, rather than importing datatables.net-dt as an ES module itself), and it reads that global whenever it constructs an instance, including every autoInit rescan, not just the first one. Load every optional dependency your app uses in one module, before the first Preline UI import in your afterNavigate callback, rather than trying to load one lazily only on the page that needs it.
preline/non-auto in Svelte when you need explicit lifecycle control. Drop to preline/plugins/<name> for a smaller surface within the same dependency, or to a standalone @preline/<name> package if you don't want preline installed at all.cleanCollection/autoInit from afterNavigate in the root layout, since it fires on the initial load and every later navigation, so it's usually the only hook you need.onMount (with tick) only for markup that can appear without a route change in between.destroy() for manual cleanup, since cleanCollection() only resets the tracking array and never calls destroy() on anything.afterNavigate callback against out-of-order async resolution on fast repeated navigation, and check whether any plugin you use renders DOM outside the route content (like File Upload) before assuming a rescan alone is enough cleanup.jquery + datatables.net-dt for Datatable, vanilla-calendar-pro + lodash for Datepicker, nouislider for Range Slider, dropzone for File Upload), and load them all before your first Preline UI import, not lazily per page.A practical guide to using Preline UI JavaScript plugins in Vue projects, including nextTick, autoInit, module imports, cleanup, and optional dependencies.
View guideA practical guide to using Preline UI JavaScript plugins in SolidJS projects, including onMount, createEffect, router rescans, module imports, cleanup, and optional dependencies.
View guide