Lightbox2 currently requires jQuery (30KB+ minified) for DOM manipulation, event handling, and animations. The entire jQuery API surface used by the library maps to well-supported native browser APIs. Removing this dependency lets developers use Lightbox2 without jQuery while keeping backwards compatibility via the existing lightbox-plus-jquery bundle.
Not a breaking change — the public API (open, close, next, prev, destroy, option) stays identical. The lightbox:open/lightbox:close/lightbox:change events switch from jQuery triggers to native CustomEvent — acceptable since they were just added in 2.12.0.
Decisions made:
- Keep UMD module format (no jQuery in wrapper)
- CSS transitions + class toggles for all animations
lightbox-plus-jquery.jsbundle just concatenates jQuery before vanilla lightboxstart()accepts both native elements and jQuery-wrapped elements
| File | Change |
|---|---|
src/css/lightbox.css |
Add transition rules, .lb-visible pattern, duration custom properties |
src/js/lightbox.js |
Rewrite all jQuery usage to vanilla DOM APIs |
package.json |
Move jquery to devDependencies, update description |
eslint.config.js |
Bump ecmaVersion to 6, remove jQuery globals, add CustomEvent/getComputedStyle/Object globals |
examples/index.html |
Add standalone (no-jQuery) usage example |
- Change
ecmaVersion: 5→ecmaVersion: 6 - Remove
$: 'readonly'andjQuery: 'readonly'from globals - Add
CustomEvent: 'readonly',getComputedStyle: 'readonly',Object: 'readonly'to globals
- Move
"jquery": "^3.7.1"fromdependenciestodevDependencies - Update description: remove "Uses jQuery."
Add to :root block:
--lb-fade-duration: 600ms;
--lb-image-fade-duration: 600ms;
--lb-resize-duration: 700ms;Add CSS transitions and .lb-visible pattern for each animated element:
| Element | Transition property | .lb-visible opacity target |
|---|---|---|
.lightboxOverlay |
opacity var(--lb-fade-duration) ease |
var(--lb-overlay-opacity) |
.lightbox |
opacity var(--lb-fade-duration) ease |
1 |
.lb-image |
opacity var(--lb-image-fade-duration) ease |
1 |
.lb-outerContainer |
width var(--lb-resize-duration) ease, height var(--lb-resize-duration) ease |
N/A (size transition, not opacity) |
.lb-dataContainer |
opacity var(--lb-resize-duration) ease |
1 |
.lb-caption |
opacity 200ms ease |
1 |
.lb-number |
opacity 200ms ease |
1 |
.lb-loader |
opacity 600ms ease |
1 |
Each element gets opacity: 0 as default state. Elements that use display: none keep it — the JS fadeIn helper handles the display→reflow→class sequence.
(function (root, factory) {
if (typeof define === 'function' && define.amd) {
define([], factory);
} else if (typeof exports === 'object') {
module.exports = factory();
} else {
root.lightbox = factory();
}
}(this, function () {fadeIn(el, duration, callback)— Setdisplay: '', force reflow, add.lb-visible. Optionaltransitionendcallback.fadeOut(el, duration, callback)— Remove.lb-visible, ontransitionendsetdisplay: none.hideEl(el)— Immediately hide (removes class +display: none), cancels any pending transition handler. Replaces jQuery's.stop(true).hide()and.hide().showEl(el)— Immediately show (setsdisplay: ''+ adds.lb-visible). Replaces jQuery's.show().
this.$lightbox → this.lightbox, this.$overlay → this.overlay, etc. Drop the $ prefix on all 14 cached element properties.
$('#lightbox').length > 0→document.getElementById('lightbox')$('<html>').appendTo($('body'))→document.body.insertAdjacentHTML('beforeend', html)$('#id')→document.getElementById('id').find('.class')→.querySelector('.class').css('padding-top')→getComputedStyle(el).paddingTop.hide().on('click', fn)→el.style.display = 'none'; el.addEventListener('click', fn)$(event.target).attr('id')→event.target.id.one('contextmenu', fn)→addEventListener('contextmenu', fn, { once: true })(or manual named-fn removal for broader compat).add($other).on('click keyup', fn)→ separateaddEventListenercalls on each element for each event type
After element caching, sync JS option durations to CSS custom properties:
var root = document.documentElement;
root.style.setProperty('--lb-fade-duration', this.options.fadeDuration + 'ms');
root.style.setProperty('--lb-image-fade-duration', this.options.imageFadeDuration + 'ms');
root.style.setProperty('--lb-resize-duration', this.options.resizeDuration + 'ms');Also update option() to sync durations when they change at runtime.
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', function() { ... });
} else { ... }Store bound handler ref in this._boundClickDelegate for cleanup in destroy():
document.body.addEventListener('click', function(event) {
var target = event.target.closest('a[rel^="lightbox"], area[rel^="lightbox"], a[data-lightbox], area[data-lightbox]');
if (target) { self.start(target); event.preventDefault(); }
});Unwrap at top: if (link && (link.jquery || (link[0] && link[0].nodeType))) { link = link[0]; }
Replace throughout:
.attr('data-lightbox')→.getAttribute('data-lightbox')$($link.prop('tagName')).filter(...)→document.querySelectorAll(tag + '[data-lightbox="' + val + '"]')$.proxy(fn, this)→fn.bind(this)— store asthis._boundTrapFocus$(document).trigger('lightbox:open', [...])→document.dispatchEvent(new CustomEvent('lightbox:open', { detail: data }))
This is the largest phase. Every .fadeIn(), .fadeOut(), .hide(), .show(), .animate(), .stop() call is converted.
.fadeIn(dur)→fadeIn(el, dur).fadeIn('slow')→fadeIn(el, 600)(jQuery 'slow' = 600ms).hide()→hideEl(el).addClass()/.removeClass()→.classList.add/remove().attr({...})→.setAttribute()calls.width(val)setter →.style.width = val + 'px'$(window).width()→window.innerWidth- Track
imageWidth/imageHeightin local variables instead of reading back via getter
Replace $outerContainer.animate({width, height}, dur, 'swing', callback) with:
- CSS
transitionon.lb-outerContainer(already in CSS from Phase 1) - Set width/height via
styleproperties - Listen for
transitionendwith atransitionDoneflag (prevents double-fire since both width and height transition) - If dimensions unchanged, call
postResize()immediately (existing logic)
.stop(true).hide()→hideEl(el).fadeIn(dur)→fadeIn(el, dur)- Custom events →
document.dispatchEvent(new CustomEvent(...))
.show()→showEl(el).css('opacity', '1')→el.style.opacity = '1'
.text(str)→.textContent = str.html(str)→.innerHTML = str.fadeIn('fast')→fadeIn(el, 200)(jQuery 'fast' = 200ms)
Store bound handler ref in this._boundKeyboardAction. Use addEventListener/removeEventListener with stored reference instead of jQuery namespaces.
.find('[tabindex]:visible').filter(fn)→querySelectorAll('[tabindex]')+ loop withoffsetParentvisibility check.first()[0]/.last()[0]→arr[0]/arr[arr.length - 1]
.off('.focustrap')→removeEventListenerwith stored_boundTrapFocusref.fadeOut(dur)→fadeOut(el, dur).trigger('focus')→.focus()
- Run
npm run build— verify all dist files generated - Run
npm run lint— verify no$references remain (caught byno-undef) - Grep source for any remaining
$usage - Update examples/index.html — add standalone usage comment
- Version bump (if desired — separate from this PR)
npm run lint— passes cleannpm run build— all 8 dist files generated- Serve project root (
npx serve .), open/examples/index.html - Manual test matrix:
- Single image click → overlay fades in, lightbox appears, image loads and fades in
- Image set navigation → container resizes smoothly, prev/next work
- Caption and image number display correctly
- Keyboard: left/right arrows navigate, Escape closes
- Close via: X button, overlay click, lightbox background click
- Focus trap: Tab cycles within lightbox only
- Right-click on image: context menu appears on the image itself
wrapAroundoption: navigation wraps from last to first- Multiple galleries via
data-lightboxgrouping work independently rel="lightbox"legacy support still works- Programmatic API:
lightbox.open('url'),lightbox.close(),lightbox.next(),lightbox.prev()
- Test
dist/js/lightbox.min.jsloaded WITHOUT jQuery — should work standalone - Test
dist/js/lightbox-plus-jquery.min.js— should work identically (jQuery present but unused by lightbox)