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 v4.2 - New components, 10+ framework guides, and quality improvements. 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.
import { nextTick } from "vue";
export default defineNuxtPlugin((nuxtApp) => {
const initPreline = async () => {
await nextTick();
const { HSStaticMethods } = await import("preline/non-auto");
HSStaticMethods.autoInit();
};
nuxtApp.hook("app:mounted", initPreline);
nuxtApp.hook("page:finish", initPreline);
});
nextTick waits for Vue's DOM flush, and page:finish lines the scan up with Nuxt page navigation.
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.
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.
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.
import { nextTick } from "vue";
import noUiSlider from "nouislider";
export default defineNuxtPlugin((nuxtApp) => {
const initRangeSlider = async () => {
await nextTick();
(
globalThis as typeof globalThis & {
noUiSlider: typeof noUiSlider;
}
).noUiSlider = noUiSlider;
const { HSRangeSlider } = await import("preline/non-auto");
HSRangeSlider.autoInit();
};
nuxtApp.hook("app:mounted", initRangeSlider);
nuxtApp.hook("page:finish", initRangeSlider);
});
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 after nextTick() from app:mounted and page:finish.@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