Using Preline UI with React + Vite
A practical guide to using Preline UI JavaScript plugins in React + Vite projects, including 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 Angular without fighting Angular rendering and routing.
Angular and Preline UI work best when Angular owns rendering and Preline UI owns the DOM behavior after the view exists. The integration is small, but the timing matters: initialize after Angular has placed the markup in the document, and clean up when Angular removes it.
The current Preline UI package gives Angular apps a module-first path: preline/non-auto for explicit scans, named plugin classes for manual instances, and internal collections that can be cleaned when Angular removes routed or component-owned markup.
Preline UI is not an Angular component library. It is a DOM-driven Tailwind CSS component system. Angular renders the template, then Preline UI scans the resulting DOM and attaches behavior to elements such as dropdowns, overlays, tabs, tooltips, and selects.
That is the reason the same markup can move between Angular, React, Vue, plain HTML, Astro, Laravel, Rails, and similar stacks. It also means Angular lifecycle hooks are part of the integration surface. If the DOM is not there yet, Preline UI has nothing to initialize.
For markup owned by a component view, ngAfterViewInit is the straightforward place to initialize Preline UI. Use preline/non-auto so initialization stays explicit after Angular has finished rendering.
import { AfterViewInit, Component, OnDestroy } from "@angular/core";
import { HSStaticMethods } from "preline/non-auto";
@Component({
selector: "app-root",
templateUrl: "./app.component.html",
})
export class AppComponent implements AfterViewInit, OnDestroy {
ngAfterViewInit() {
HSStaticMethods.autoInit(["dropdown", "overlay"]);
}
ngOnDestroy() {
HSStaticMethods.cleanCollection(["dropdown", "overlay"]);
}
}
autoInit is collection-aware. Calling it again does not create duplicate dropdown instances for the same node; it initializes markup that has not been initialized yet.
Route changes are the common case where Angular replaces a large part of the page. Listen for NavigationEnd, then run autoInit against the new route markup, but do it through afterNextRender() rather than a microtask or a fixed delay. NavigationEnd fires once Angular has resolved which route won, which is not the same moment Angular finishes patching the DOM for it; a plain queueMicrotask (or a setTimeout) can still run before that patch lands, so autoInit() scans a DOM that does not have the new route's elements yet. afterNextRender() is Angular's own API for "after a render has actually been committed," and is the reliable way to sequence this.
import { Component, DestroyRef, Injector, OnDestroy, OnInit, afterNextRender, inject } from "@angular/core";
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
import { NavigationEnd, Router, RouterOutlet } from "@angular/router";
import { filter } from "rxjs/operators";
import { HSStaticMethods } from "preline/non-auto";
@Component({
selector: "app-root",
imports: [RouterOutlet],
template: "<router-outlet />",
})
export class AppComponent implements OnInit, OnDestroy {
private readonly router = inject(Router);
private readonly injector = inject(Injector);
private readonly destroyRef = inject(DestroyRef);
ngOnInit() {
this.router.events
.pipe(
filter((event): event is NavigationEnd => event instanceof NavigationEnd),
takeUntilDestroyed(this.destroyRef)
)
.subscribe(() => {
afterNextRender(() => HSStaticMethods.autoInit(), { injector: this.injector });
});
}
ngOnDestroy() {
HSStaticMethods.cleanCollection();
}
}
The subscription itself is set up in ngOnInit rather than the constructor, since Angular's own style guide reserves the constructor for dependency injection, and it is torn down through takeUntilDestroyed rather than a manually tracked Subscription. The important part beyond that is not the exact hook name; it is the order. Angular must finish putting the route view into the DOM before Preline UI scans it.
For a full Preline UI installation, preline/non-auto is the safest Angular 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.autoInit(["dropdown", "overlay"]);
HSStaticMethods.cleanCollection(["dropdown", "overlay"]);
If a component owns a single plugin root, importing the plugin class from preline/non-auto is more direct. It stays on the declared package surface while avoiding automatic page-load initialization.
import { HSDropdown } from "preline/non-auto";
HSDropdown.autoInit();
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 an Angular feature only needs one interactive primitive.
npm install @preline/dropdown
import HSDropdown from "@preline/dropdown/non-auto";
HSDropdown.autoInit();
In that single-package setup, the auto entry is import "@preline/dropdown". Use it for simple pages. In Angular components and routed apps, the /non-auto entry is easier to reason about because initialization stays aligned with Angular lifecycle hooks.
The single-plugin packages do not currently ship a declaration file for their /non-auto entry, so a strict Angular build reports TS7016: Could not find a declaration file for module '@preline/dropdown/non-auto' for the import above. Add a narrow ambient declaration for it. See Keep TypeScript declarations narrow below for the same pattern applied to the main preline package's plugin-specific paths:
declare module "@preline/dropdown/non-auto" {
export { default } from "@preline/dropdown";
}
autoInit is good for route-level scans. Manual instances are better when an Angular component owns one specific plugin root and can destroy it directly in ngOnDestroy.
import { AfterViewInit, Component, ElementRef, OnDestroy, ViewChild } from "@angular/core";
import { HSDropdown, type IHTMLElementFloatingUI } from "preline/non-auto";
@Component({
selector: "app-dropdown",
template: `
<div #dropdownRoot class="hs-dropdown relative inline-flex">
...
</div>
`,
})
export class DropdownComponent implements AfterViewInit, OnDestroy {
@ViewChild("dropdownRoot", { static: true })
dropdownRoot!: ElementRef<HTMLElement>;
private dropdown?: InstanceType<typeof HSDropdown>;
ngAfterViewInit() {
this.dropdown = new HSDropdown(this.dropdownRoot.nativeElement as unknown as IHTMLElementFloatingUI);
}
ngOnDestroy() {
this.dropdown?.destroy();
}
}
In strict Angular builds, keep any package declaration workaround local and narrow instead of disabling type checking for the whole app. For example, if you intentionally use a plugin-specific full-package path such as preline/plugins/dropdown-non-auto before matching declaration files ship, declare only that module.
declare module "preline/plugins/dropdown-non-auto" {
const HSDropdown: {
new (el: HTMLElement, options?: Record<string, unknown>): {
destroy(): void;
};
autoInit(): void;
};
export default HSDropdown;
}
If your build reports a missing vanilla-calendar-pro/types declaration from the full preline/non-auto types, install the optional datepicker dependency or add a temporary declaration for that module. Keep that declaration close to your Preline integration so it is easy to remove when package types are updated.
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 CSS classes do not need to be merged into Preline UI styling.
preline/non-auto in Angular when you need explicit lifecycle control.HSStaticMethods.autoInit() after Angular has rendered the component or route markup.NavigationEnd plus afterNextRender() for routed Angular apps, not a fixed setTimeout or a bare queueMicrotask, since neither reliably waits for Angular's DOM patch to land.ViewChild when a component owns one plugin root.destroy() for manual instances and cleanCollection() when removing initialized groups of markup.datatables.net-dt for Datatable or noUiSlider for Range Slider, and load them before importing preline/non-auto, since several plugins check for their global once, at that module's first evaluation.A practical guide to using Preline UI JavaScript plugins in React + Vite projects, including autoInit, module imports, cleanup, and optional dependencies.
View guideA practical guide to using Preline UI JavaScript plugins in Vue projects, including nextTick, autoInit, module imports, cleanup, and optional dependencies.
View guide