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 Nuxt without fighting SSR, route transitions, and Vue lifecycle timing.
Nuxt is Vue with an SSR and routing layer on top, so the Preline UI integration is mostly about keeping browser-only code out of the server build. Vue renders the markup. Nuxt decides when pages mount and finish navigation. Preline UI should scan the DOM only after that client-side work is done.
The current Preline UI package exposes browser-side module entries that fit Nuxt's client boundary: preline/non-auto for explicit scans, named plugin classes for manual instances, and single-plugin packages for smaller client-only surfaces.
Preline UI is a DOM-driven Tailwind CSS component system. Nuxt can render the page on the server, hydrate it on the client, and replace route content without a full reload. Preline UI should only touch the browser DOM after hydration or a client-side page update.
That is why the integration should not depend on global page-load side effects. Use client-only entry points, initialize after Vue has flushed the DOM, and keep manual plugin instances tied to the Vue component that owns their root element.
Put page-level initialization in a Nuxt client plugin. The .client.ts suffix keeps the plugin out of SSR, and the dynamic import keeps Preline UI loading aligned with the browser lifecycle.
export default defineNuxtPlugin((nuxtApp) => {
nuxtApp.hook("page:finish", async () => {
const { HSStaticMethods } = await import("preline/non-auto");
HSStaticMethods.cleanCollection();
HSStaticMethods.autoInit();
});
});
page:finish fires after Nuxt's initial page mount as well as after every client-side route transition, so a single hook covers both cases and no separate app:mounted hook or nextTick() delay is needed. cleanCollection() clears Preline UI's bookkeeping before the rescan so elements from a previous page's DOM (already removed by Vue on navigation) don't linger in the collection.
Nuxt can replace page content during client-side navigation. Running autoInit from page:finish gives Preline UI a clean chance to scan the new page DOM.
autoInit is collection-aware. It filters stale nodes that are no longer in the document and skips elements that already have plugin instances, so repeated page-level scans are expected.
If a Nuxt surface only needs a few plugins, pass collection keys such as dropdown, overlay, select, tooltip, tabs, or range-slider.
HSStaticMethods.autoInit(["dropdown", "overlay"]);
For a full Preline UI installation, preline/non-auto is the practical Nuxt default. It gives you HSStaticMethods and named plugin classes without automatic initialization on page load.
const { HSStaticMethods } = await import("preline/non-auto");
HSStaticMethods.autoInit(["dropdown", "overlay"]);
HSStaticMethods.cleanCollection(["dropdown", "overlay"]);
Avoid static Preline UI imports in ordinary Nuxt components that render on the server. Use a .client.vue component, a .client.ts plugin, or a dynamic import inside onMounted.
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 a Nuxt component or route only needs one or two interactive primitives.
npm install @preline/dropdown
<script setup lang="ts">
import { nextTick, onMounted } from "vue";
import HSDropdown from "@preline/dropdown/non-auto";
onMounted(async () => {
await nextTick();
HSDropdown.autoInit();
});
</script>
In that single-package setup, the auto entry is import "@preline/dropdown". It is useful for simple static pages. In Nuxt, /non-auto is usually easier because initialization stays tied to client hydration and route timing.
The /non-auto subpath currently ships without its own type declarations in @preline/dropdown, @preline/overlay, @preline/select, and @preline/range-slider, since each package only publishes index.d.ts, matched to the bare, auto-init entry. Under a strict Nuxt project (strict: true, the framework's own default), importing from /non-auto fails with TS7016: Could not find a declaration file for module '@preline/<plugin>/non-auto'. Until the packages ship a matching non-auto.d.ts, add an ambient module declaration for the specific subpath you use:
declare module "@preline/dropdown/non-auto";
autoInit is convenient for page-level scans. A manual instance is better when one Nuxt component owns one plugin root and can destroy it locally. Make the component client-only when it imports a Preline UI class statically.
<script setup lang="ts">
import { nextTick, onBeforeUnmount, onMounted, ref } from "vue";
import {
HSDropdown,
type IHTMLElementFloatingUI,
} from "preline/non-auto";
const dropdownRef = ref<HTMLDivElement | null>(null);
let dropdown: HSDropdown | null = null;
onMounted(async () => {
await nextTick();
if (dropdownRef.value) {
dropdown = new HSDropdown(
dropdownRef.value as unknown as IHTMLElementFloatingUI,
);
}
});
onBeforeUnmount(() => {
dropdown?.destroy();
dropdown = null;
});
</script>
<template>
<div ref="dropdownRef" class="hs-dropdown relative inline-flex">
...
</div>
</template>
The cast is only for strict TypeScript. At runtime the plugin receives the actual dropdown root element and augments it with Floating UI internals.
Preline UI stores plugin instances in internal collections such as window.$hsDropdownCollection. That registry lets plugins coordinate without Vue or Nuxt context and is part of why the same codebase works in plain HTML, React, Vue, Angular, Svelte, SolidJS, Next.js, and Nuxt.
In Nuxt, call destroy() for manual instances. For page-level scans, autoInit already filters removed nodes for the plugin collections it scans.
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 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.
Datepicker also needs lodash on window._, not just Vanilla Calendar Pro. This is easy to miss: Datepicker's own availability check only looks for window.VanillaCalendarPro, but its option-merging logic calls _.merge/_.mergeWith unconditionally as soon as an instance is constructed. Skip lodash and HSStaticMethods.autoInit() throws a ReferenceError: _ is not defined from inside Preline UI's own bundle the moment it reaches a Datepicker element. File Upload has the same dependency on lodash for a different reason (general utility functions), so if both plugins are in use, one window._ assignment covers both.
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 general Nuxt Preline plugin.
npm install nouislider
import noUiSlider from "nouislider";
export default defineNuxtPlugin((nuxtApp) => {
(
globalThis as typeof globalThis & {
noUiSlider: typeof noUiSlider;
}
).noUiSlider = noUiSlider;
nuxtApp.hook("page:finish", async () => {
const { HSRangeSlider } = await import("preline/non-auto");
HSRangeSlider.autoInit();
});
});
For a late-loaded optional plugin, use the direct plugin class like HSRangeSlider. If you want to use HSStaticMethods.autoInit(["range-slider"]) instead, expose noUiSlider in the same first loader before any other preline/non-auto import happens.
.client.ts plugins or .client.vue components.preline/non-auto when you need explicit lifecycle control.autoInit from page:finish, since it fires on the initial page mount too, so no separate app:mounted hook is needed.@preline/dropdown/non-auto when a surface only needs one plugin.destroy() for reusable client-only components that own one plugin root.preline/non-auto import when using HSStaticMethods, or initialize late optional plugins with their direct classes.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 Next.js projects, including client components, App Router navigation, module imports, cleanup, and optional dependencies.
View guide