Update v4.2 - New components, 10+ framework guides, and quality improvements. Visit Changelog

Ember.js

Using Preline UI with Ember.js

A practical guide to wiring Preline UI into Ember applications without treating DOM plugins as global application state.

Ember is productive because templates, routing, and component lifetimes are explicit. Preline UI fits that model when you let Ember render first, then attach Preline UI behavior to the real browser DOM.

The important distinction is initialization timing. Preline UI plugins read DOM attributes, create instances, and keep internal collections so plugins can cooperate across plain HTML, server-rendered pages, and framework apps. In Ember, use preline/non-auto, run autoInit after route renders, and create manual instances for component roots that need precise teardown.

Start with the Ember model

Preline UI is a DOM-driven Tailwind CSS component system. Ember owns rendering, route transitions, and component cleanup. Preline UI should run after Ember has placed the final markup in the document, not as a top-level side effect that guesses when the page is ready.

That is why preline/non-auto is the better default for Ember apps. It imports the plugin classes and HSStaticMethods without page-load auto-initialization, so your route or modifier decides when to scan and when to destroy.

Reinitialize after route changes

Ember route transitions can replace large parts of the page without a full document reload. Listen to the router service and schedule autoInit after the route has changed, so Preline UI scans the new DOM instead of the outgoing one.

app/instance-initializers/preline.js
                        
                          import { next } from "@ember/runloop";

                          export function initialize(appInstance) {
                            const router = appInstance.lookup("service:router");

                            const initPreline = () => {
                              next(async () => {
                                const { HSStaticMethods } = await import("preline/non-auto");

                                HSStaticMethods.autoInit();
                              });
                            };

                            router.on("routeDidChange", initPreline);
                            initPreline();
                          }

                          export default {
                            initialize,
                          };
                        
                      

If one route only uses a narrow set of plugins, use a targeted scan such as HSStaticMethods.autoInit(["dropdown", "overlay"]). That keeps route rescans explicit and avoids touching components that are not present.

Use a modifier for component-owned DOM

A route-wide scan is convenient for static markup. A modifier is better when an Ember component owns one plugin root and can clean it up when the element leaves the DOM.

app/modifiers/hs-dropdown.js
                        
                          import { modifier } from "ember-modifier";

                          export default modifier((element) => {
                            let dropdown;
                            let destroyed = false;

                            import("preline/non-auto").then(({ HSDropdown }) => {
                              if (destroyed) return;

                              dropdown = new HSDropdown(element);
                            });

                            return () => {
                              destroyed = true;
                              dropdown?.destroy();
                            };
                          });
                        
                      
app/components/user-menu.hbs
                        
                          <div {{hs-dropdown}} class="hs-dropdown relative inline-flex">
                            <button
                              id="hs-user-menu"
                              type="button"
                              class="hs-dropdown-toggle py-2 px-3 inline-flex items-center gap-x-2 text-sm font-medium rounded-lg border border-stone-200 bg-white text-stone-800 shadow-2xs hover:bg-stone-50 focus:outline-hidden focus:bg-stone-50 dark:bg-neutral-900 dark:border-neutral-700 dark:text-white dark:hover:bg-neutral-800 dark:focus:bg-neutral-800"
                              aria-haspopup="menu"
                              aria-expanded="false"
                              aria-label="Open menu"
                            >
                              Account
                            </button>

                            <div class="hs-dropdown-menu hidden min-w-40 bg-white shadow-md rounded-lg p-1 dark:bg-neutral-800" role="menu" aria-labelledby="hs-user-menu">
                              <a class="flex items-center gap-x-3.5 py-2 px-3 rounded-lg text-sm text-stone-800 hover:bg-stone-100 focus:outline-hidden focus:bg-stone-100 dark:text-neutral-200 dark:hover:bg-neutral-700 dark:focus:bg-neutral-700" href="/profile">
                                Profile
                              </a>
                            </div>
                          </div>
                        
                      

Choose imports by lifecycle control

Use preline/non-auto for app-level route rescans and named classes. It gives you one module import surface for HSStaticMethods, HSDropdown, HSOverlay, HSSelect, and the other plugin classes.

Targeted scan
                        
                          const { HSStaticMethods } = await import("preline/non-auto");

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

Direct non-auto plugin imports are useful when a modifier or service only needs one class and does not need the full static-method surface.

Direct plugin class
                        
                          import HSDropdown from "preline/plugins/dropdown-non-auto";

                          const dropdown = new HSDropdown(element);
                        
                      

Single plugin packages keep bundles focused

Preline UI plugins can also be consumed as single dependencies, for example @preline/dropdown, @preline/overlay, @preline/select, or @preline/range-slider. This is the cleanest shape when an Ember modifier only needs one plugin.

Use this shape when those single packages are available in your dependency set. In the current monolithic package, the equivalent direct non-auto import is preline/plugins/dropdown-non-auto.

app/modifiers/hs-dropdown.js
                        
                          import { modifier } from "ember-modifier";
                          import HSDropdown from "@preline/dropdown/non-auto";

                          export default modifier((element) => {
                            const dropdown = new HSDropdown(element);

                            return () => {
                              dropdown.destroy();
                            };
                          });
                        
                      

Manual instances are the cleanup boundary

Preline UI stores plugin instances in internal browser collections. That registry is what lets separate plugins find each other in plain HTML and framework apps. In Ember, it means a component that creates an instance should destroy it when Ember removes the element.

Prefer modifiers for this shape. The element is the plugin root, the modifier creates the instance, and the returned cleanup function calls destroy().

Clean route-level collections intentionally

If you rely on app-level autoInit after route changes, call cleanCollection before rescanning plugin types that may have been removed by the previous route. Manual modifier instances should still use destroy() instead.

Route rescan with cleanup
                        
                          const { HSStaticMethods } = await import("preline/non-auto");

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

Optional dependencies only belong to 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; Preline UI does not need noUiSlider CSS to make the headless JavaScript behavior work.

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. A late optional plugin can also be initialized through its direct class.

app/modifiers/hs-range-slider.js
                        
                          import { modifier } from "ember-modifier";
                          import noUiSlider from "nouislider";

                          globalThis.noUiSlider = noUiSlider;

                          export default modifier((element) => {
                            let rangeSlider;
                            let destroyed = false;

                            import("preline/non-auto").then(({ HSRangeSlider }) => {
                              if (destroyed) return;

                              HSRangeSlider.autoInit();
                              rangeSlider = HSRangeSlider.getInstance(element, true)?.element;
                            });

                            return () => {
                              destroyed = true;
                              rangeSlider?.destroy();
                            };
                          });
                        
                      

The practical checklist

  • Use preline/non-auto for Ember apps so initialization follows Ember rendering.
  • Run HSStaticMethods.autoInit() after route changes when markup is route-owned.
  • Use element modifiers for component-owned plugin roots and call destroy() in the modifier cleanup.
  • Use targeted scans such as autoInit(["dropdown", "overlay"]) when a route only needs specific plugins.
  • When available in your dependency set, use single plugin packages such as @preline/dropdown/non-auto when a modifier only needs one plugin class.
  • 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.