A zero-build JavaScript component system using jQuery and Bootstrap.
Themestrap is a rich UI component set available without a build pipeline. No Webpack, no npm, no compilation step. Every component is a self-contained jQuery plugin that:
- Lazy-initializes via
IntersectionObserver— only activates when the element enters the viewport - Lazy-injects its own CSS the first time it runs — no separate stylesheet to enqueue
- Exposes a full
destroy()method for clean teardown and re-init - Reads configuration from
data-plugin-optionsattributes — no JavaScript required for basic use
All plugins share the window.themestrap namespace and load through two files: themestrap.js (all plugin class definitions) and themestrap.init.js (auto-wiring via IntersectionObserver).
Every plugin follows a strict five-stage lifecycle:
initialize -> setData -> setOptions -> build -> events
| Convention | Pattern |
|---|---|
| Filename | themestrap.plugin.{name}.js |
| IIFE shape | Triple-paren (function($, window, document, undefined) { ... }(jQuery, window, document)) |
| jQuery method | $.fn.themestrapPlugin{Name} |
| Instance key | __plugin{Name} (stored via $el.data()) |
| CSS injection | Lazy, from build(), guarded by a STYLE_ID check |
| Mutual exclusion events | Hyphen-separated: ts-popover-opened, ts-dialog-opened |
<!-- Auto-init: just add the data attribute -->
<div data-plugin-accordion data-plugin-options='{"speed": 300}'></div>
<!-- Manual init -->
<div id="my-accordion"></div>
<script>
$('#my-accordion').themestrapPluginAccordion({ speed: 300 });
</script>
<!-- Access the instance -->
<script>
const instance = $('#my-accordion').data('__pluginAccordion');
instance.destroy();
</script>Plugins use one of four initialization strategies depending on their needs:
| Strategy | Function | Use when |
|---|---|---|
| Simple lazy | intObsInit(sel, fn) |
Plugin reads only data-plugin-options; no defaults merging |
| Defaults merging | dynIntObsInit(sel, fn, defaults) |
Plugin has a .defaults static property |
| Interaction-lazy | execOnceThroughEvent(sel, event, cb) |
Defer init until first hover/focus (e.g. lightbox) |
| Immediate | $().each() in $(() => {}) |
Must run on DOMReady regardless of viewport |
The Module Loader (themestrap.loader.js + themestrap.modules.js) adds lazy fetching on top of the framework's lazy initialization. Rather than bundling all plugins and their vendor dependencies into every page, each component declares what it needs via a data-module attribute and the loader resolves, fetches, and activates only what that page actually uses.
themestrap.js -> themestrap.loader.js -> themestrap.modules.js -> your page declares data-module
On DOM ready the manifest calls loader.scan(), which:
- Scans: collects every
data-modulevalue on the page - Resolves: walks each module's
depsgraph depth-first; shared vendor libraries are fetched only once - Fetches: loads CSS and JS in dependency order with
cache: trueso returning visitors hit the browser cache - Activates: runs the same
intObsInit/dynIntObsInit/ event wiring thatthemestrap.init.jsuses, so a plugin fetched after DOM ready still gets its IntersectionObserver
Warning
One owner per plugin.
If the loader manages a plugin, that plugin must not also be statically included or wired in themestrap.init.js. Two owners means two init passes.
1. Declare it in themestrap.modules.js:
const loader = themestrap.loader({
basePath: '/js/components/',
mode: document.documentElement.dataset.env === 'prod' ? 'prod' : 'dev'
});
loader
.define('owl', {
src: 'vendor/owl.carousel/js/owl.carousel.min.js',
css: 'vendor/owl.carousel/css/owl.carousel.min.css'
})
.definePlugin('carousel', {
src: 'themestrap.plugin.carousel.js',
deps: ['owl'],
selector: '[data-plugin-carousel]',
method: 'themestrapPluginCarousel',
strategy: 'intObs'
});
$(() => loader.scan());2. Declare the need in markup:
<div data-module="carousel"
data-plugin-carousel
data-plugin-options='{"loop": true, "items": 3}'>
<!-- slides -->
</div>A page without that element fetches none of it.
| Method | Signature | Purpose |
|---|---|---|
define |
(name, {src, css, deps, init}) |
Register any module: JS file, CSS file, dependencies, and/or an init callback |
definePlugin |
(name, cfg) |
Register a Themestrap $.fn plugin; generates init to run the framework's lazy-init wiring |
require |
([names]) -> promise |
Resolve, fetch, and activate named modules after DOM ready |
load |
(name) -> promise |
Load one module and its deps, once; in-flight is shared |
scan |
(context = document) |
Find every [data-module] in context and require() them |
on / emit |
(event, fn) / (event, data) |
Tiny event bus; emits loaded:<name> as each module settles |
definePlugin's strategy field mirrors the wiring in themestrap.init.js:
| Strategy | Maps to | Use for | Extra field |
|---|---|---|---|
intObs (default) |
intObsInit(sel, method) |
Most plugins — init when element nears viewport | — |
dynIntObs |
dynIntObsInit(sel, method, defaults) |
Plugins whose defaults carry accY / forceInit |
defaultsClass |
event |
execOnceThroughEvent(...) |
Defer until a user event (e.g. lightbox on hover) | event |
$.get(url, html => {
const $frag = $(html).appendTo('#results');
themestrap.loader().scan($frag[0]); // fetch + activate only what arrived
});Reach for the loader when a plugin depends on a heavy vendor library (Isotope, Owl Carousel, Slider Revolution) that most pages never use, or when you ship separate-file plugins rather than one concatenated bundle. Skip it when the plugin is part of your always-loaded core bundle or its library is needed above the fold on nearly every page.
Some plugins depend on a third-party libraries:
| Vendor library | Module name | Plugins |
|---|---|---|
| easyPieChart | easypiechart |
chartCircular |
| jquery.visible | jquery-visible |
floatElement, parallax, icon, nav |
| jquery.cookie | jquery-cookie |
gdpr, gdprWrapper |
| Vivus | vivus |
icon |
| observe-element-in-viewport | observe-element-in-viewport |
inViewportStyle |
| vide | vide |
videoBackground |
| hover3d | hover3d |
hoverEffect (opt) |
| jquery.matchHeight | matchheight |
matchHeight |
| jquery.easing | jquery-easing |
progressBar, sectionScroll, scrollToTop, nav |
| nanoScrollerJS | nanoscroller |
scrollable |
| jquery.pin | jquery-pin |
sticky |
| jquery-validation | jquery-validation |
validation, newsletter, search |
| Slider Revolution | rs-plugin |
revolutionSlider |
Most of the components need no third-party library; they inject their own CSS and use only jQuery, Bootstrap, or vanilla APIs. Register them with definePlugin only if you also want lazy fetching; they have no deps:
<div
data-plugin-carousel
data-plugin-options='{"loop": true, "autoplay": true, "autoplayTimeout": 5000}'
></div>Options are read and merged over the plugin's .defaults. Single-quoted attribute, double-quoted JSON keys.
Add the .manual class to prevent themestrap.init.js from auto-initializing an element:
<div class="owl-carousel manual" id="my-carousel"></div>
<script>
$('#my-carousel').themestrapPluginCarousel({ loop: false });
</script><button
data-reinit-plugin="__pluginCarousel"
data-reinit-plugin-function="themestrapPluginCarousel"
data-reinit-plugin-element="#my-carousel"
data-reinit-plugin-options='{"loop": false}'
>Reinit</button>Requires the plugin to implement destroy().
- 60+ plugins
- A guide for each plugin located under the docs folder
- 3 starter templates showing how to use the plugins in the wild
- IntersectionObserver lazy-init pipeline (
themestrap.init.js) - Module Loader (
themestrap.loader.js+themestrap.modules.js) with full vendor dependency map - Full
destroy()/ re-init support on all plugins
- Rewrite all plugins with dependencies to be self-contained. (in progress)
- Integration with MODX Revolution. Will include dedicated snippets and chunks for almost every component.
- jQuery 4 compatibility — A
themestrap.component.jsbase class with auto-tracked teardown, reactivesetState(), and adefineComponent()factory was designed and prototyped. A few plugins have been migrated as a proof of concept; broader rollout is pending.
MIT