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

Nuxt

Using Preline UI with Nuxt

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.

Start with the Nuxt mental model

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.

Create a client-only Preline plugin

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.

plugins/preline.client.ts
                        
                          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.

Reinitialize after Nuxt page updates

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.

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

Choose imports by lifecycle control

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.

Client-only code
                        
                          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.

Single plugin packages keep small Nuxt surfaces focused

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.

Terminal
                        
                          npm install @preline/dropdown
                        
                      
Dropdown.client.vue
                        
                          <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:

global.d.ts
                        
                          declare module "@preline/dropdown/non-auto";
                        
                      

Use client-only components for manual instances

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.

Dropdown.client.vue
                        
                          <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.

Cleanup matters because Preline UI keeps registries

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.

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. 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.

Terminal
                        
                          npm install nouislider
                        
                      
plugins/range-slider.client.ts
                        
                          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.

The practical checklist

  • Keep Preline UI initialization in Nuxt client-only files: .client.ts plugins or .client.vue components.
  • Use preline/non-auto when you need explicit lifecycle control.
  • Run autoInit from page:finish, since it fires on the initial page mount too, so no separate app:mounted hook is needed.
  • When available in your dependency set, use single plugin packages such as @preline/dropdown/non-auto when a surface only needs one plugin.
  • Use manual instances and destroy() for reusable client-only components that own one plugin root.
  • Load optional third-party libraries before the first preline/non-auto import when using HSStaticMethods, or initialize late optional plugins with their direct classes.

© 2026 Preline Labs.