The most confusing part of this bug was the part that worked. Click a photo in the property gallery and page scroll locks instantly, exactly what a lightbox is supposed to do when it opens. But no photo appears. The screen looks the same as before, except now it cannot be scrolled and there is no close button to escape with.
Half the mechanism runs and half does not, and that is actually the clearest clue available. Locking scroll is JavaScript's job, so the handler definitely fired. Only one final step never happened: the lightbox never became visible.
Two sides of a contract that never met
The cause sits in the state marker two different files used for the same thing. The JS side toggled the class .gallery-lightbox--open, while the gallery stylesheet revealed the lightbox through the attribute [data-state="open"]. So clicking a photo locked scroll and showed nothing at all.
// JS marks the open state with a class
lightbox.classList.add('gallery-lightbox--open');
document.body.style.overflow = 'hidden';/* The stylesheet reveals the lightbox on an attribute, not on that class */
.gallery-lightbox[data-state="open"] {
/* the rules that make the lightbox visible */
}What makes this kind of drift slip past code review is that both files are correct. The JS reads fine on its own, and so does the CSS. The only wrong thing is the agreement between them, and nothing anywhere enforces that agreement. No compiler cares, no linter knows this class once had a counterpart in the stylesheet.
The lightbox itself is DOM driven: it reads nodes marked [data-lightbox-image] to know which photos it should display. So there is more than one implicit contract that has to stay in sync inside a feature this small.
This was a regression, not an unfinished feature
In theme version 1.14.1 the gallery lightbox was recorded as opening, and that was not a conclusion drawn from reading code but the result of direct verification. The next version, 1.15.0, was also verified live by real clicks, with eight fixes clicked one by one. The frozen lightbox fix only landed in the version after that, 1.15.1.
So this was not a half finished feature that had never worked. It was something that did work once, and then one side of it drifted on its own in the middle of a tight run of releases.
The fix
What needs repairing is not the JS side or the CSS side, it is the distance between them. One state marker, used by both.
The fix shipped in release 1.15.1, whose upload manifest contained five files. One of them was the lightbox CSS in compiled form, gallery.min.css, plus a cache purge. The lightbox JS file was not in that manifest, so the side that got re-shipped was the stylesheet.
That min form is not a cosmetic detail. The theme's asset helper auto prefers dist/*.min.{ext}, so editing a source file without running the build ships nothing at all to production. Visual checking through a screenshot is treated as mandatory on this project rather than optional, because network checks alone once passed twice by luck and missed real regressions.
One thing I want to record as it stands: version 1.15.1 was re-uploaded by the client, and at the time the note was written this fix had not yet been click verified on production.
The same selector, a second drift
The same lightbox was hit again, this time in its arrows. The arrow selectors in the stylesheet were changed to __prev and __next, previously __nav--prev and __nav--next, along with a rule for __overlay and a bigger close glyph.
/* Before the fix: */
/* CSS styles .gallery-lightbox__nav--prev / __nav--next */
/* JS reaches for .gallery-lightbox__prev / __next */That arrow and overlay fix landed in version 1.16.0, and this one was genuinely verified live on production. In the list of click verification results from test mode, the lightbox arrows and overlay were recorded as live.
Why this is a bug class, not a single incident
The __nav-- versus __prev/next drift is recorded as one of three same class bugs that had already appeared on this project, alongside a closest bug in the function that reflects booking state and a "deferred" skip in photo upload. A wider audit found four separate flows broken because the JS had silently evolved away from server side reality.
The conclusion became a project rule: every interactive control has to actually be clicked in a real browser and its side effect confirmed, because code review cannot catch handlers that exit early in silence.
The prevention plan is more mechanical than relying on a careful reader. Statically map every data-action, button, form and link to its JS handler, then to its REST endpoint or route, and flag contract drift (missing handler, 404 route, selector mismatch, deferred or TODO stubs) before anything gets clicked one by one. The gallery lightbox on the property page itself sits on the list of controls flagged as drift prone and not yet thoroughly click verified.
Takeaways
The client put it most compactly. The observation: every time something is actually clicked, another bug turns up, which means there are still many click functions never tested at all. The request was not ambiguous either, test by clicking for real rather than by reading code, and leave nothing behind. Within that same session, audit thoroughness was escalated three times, and static screenshots do not count as an audit.
For me the lesson lands here. Classes and attributes are both legitimate state markers, and neither one is the wrong choice. What is dangerous is letting two files pick different markers for the same state, because nothing will ever tell you. The bug does not surface as an error, it surfaces as half a feature working, and that working half is exactly what convinces you everything is fine.