Most core plugins do not use jQuery or any other third-party library. Dropdowns, overlays, tooltips, popovers, tabs, and similar components use plain JavaScript. Positioning behavior uses @floating-ui/dom.
A handful of plugins do lean on a third-party library that must already exist as a global on window: Datatable needs jquery and datatables.net, Range Slider needs nouislider, Datepicker needs lodash and vanilla-calendar-pro, and File Upload needs lodash and dropzone.
Install whichever of these a page actually needs, then copy the built file into static/js/ next to preline.js:
npm install jquery datatables.net lodash vanilla-calendar-pro dropzone nouislider
cp node_modules/jquery/dist/jquery.min.js static/js/
cp node_modules/datatables.net/js/dataTables.min.js static/js/
cp node_modules/lodash/lodash.min.js static/js/
cp node_modules/vanilla-calendar-pro/index.js static/js/vanilla-calendar-pro.js
cp node_modules/dropzone/dist/dropzone-min.js static/js/
cp node_modules/nouislider/dist/nouislider.min.js static/js/
The detail that actually breaks Flask projects is load order, not just presence. Each of those plugins checks for its dependency on window once, at the moment the Preline UI bundle itself is parsed, not lazily and not re-checked later. Load every dependency's script tag before preline.js (or before the Vite entry that imports Preline UI) on any page that renders that plugin's markup. If the dependency loads after, or is fetched asynchronously and only resolves after Preline UI's script has already run, that plugin is disabled for the rest of the page: loading the dependency afterward and calling autoInit() again does not recover it, since the check already happened.
<!-- Load every optional dependency the page needs before Preline UI -->
<script src="{{ url_for('static', filename='js/jquery.min.js') }}"></script>
<script src="{{ url_for('static', filename='js/dataTables.min.js') }}"></script>
<script src="{{ url_for('static', filename='js/lodash.min.js') }}"></script>
<script src="{{ url_for('static', filename='js/vanilla-calendar-pro.js') }}"></script>
<script src="{{ url_for('static', filename='js/dropzone-min.js') }}"></script>
<script src="{{ url_for('static', filename='js/nouislider.min.js') }}"></script>
<script src="{{ url_for('static', filename='js/preline.js') }}"></script>
Datatable needs one more thing beyond the scripts: datatables.net renders its own default search box, page-length dropdown, and pagination row, on top of the ones Preline UI's own markup already provides. Neither variants.css nor datatables.net's JavaScript hides the duplicates for you, so add this rule to your own CSS:
/* Hide datatables.net's own search, length, and paging UI in favor of Preline UI's */
.dt-layout-row:has(.dt-search),
.dt-layout-row:has(.dt-length),
.dt-layout-row:has(.dt-paging) {
display: none !important;
}
Range Slider is styled entirely by Preline UI's own Tailwind CSS classes, passed straight into the plugin's cssClasses option, so noUiSlider's own stylesheet is never needed.
Datepicker's calendar styles are separate from the aggregate variants.css import. The main preline package exports them directly, so import the utility stylesheet from its public package path:
@import "preline/datepicker-styles-utility.css";
Without it, the Datepicker plugin still opens and tracks a selected date correctly, since that behavior lives in the JavaScript, but the calendar renders with no rounded date pills, no selected/today highlight, and no spacing, because none of that comes from variants.css.