SmartPhoto: a responsive image viewer whose group list reconciles with the DOM on open
The most easy to use responsive image viewer especially for mobile devices
At a glance
- What is it?
- SmartPhoto is an MIT licensed TypeScript library that turns marked-up links or a data array into a gesture-driven lightbox, and its distinguishing behaviour is a delegated click listener plus a reconciliation pass that syncs photo groups with the current DOM just before a photo opens. It suits teams that add images to the page after load, and the README is unusually explicit about where that behaviour stops working.
- Who is it for?
- SmartPhoto fits a site whose image list grows as the user scrolls, because the delegated listener and the reconciliation pass mean newly appended anchors are picked up without any extra call, and the data source mode covers the case where images come from an API instead of the markup.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 4 days ago.
- What is it written in?
- Mainly TypeScript, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 25, 2026, and from our analysis. They are not legal advice.
Editorial analysis
A delegated listener that catches images added after construction
SmartPhoto is a responsive image viewer, described as the most easy to use responsive image viewer especially for mobile devices, and the mechanism behind that ease is a single delegated click listener.
The README says it plainly. When SmartPhoto is constructed with a CSS selector string, clicks are handled via a single delegated listener, so elements added to the page after construction, for example by Ajax or infinite scroll, are picked up automatically just by clicking them, with no need to call addItem() or addNewItem() manually.
That single design decision removes the most common failure mode of a lightbox built on a list of elements. A viewer that queries the DOM at construction time will not know about the tenth image loaded on scroll, and the usual fix is a manual registration call after every append, which is exactly the kind of bookkeeping that gets forgotten and produces a viewer that silently stops working halfway down a gallery.
The library also draws a clear line around that convenience. When a NodeList or Element[] is passed, or in data source mode, add and remove items explicitly via addItem() and addNewItem(). So the automatic behaviour belongs to the selector-string form only, and the two other forms are back to explicit registration. That is a defensible boundary: if you hand the library a static array of elements, it has no way to know what the page does later.
The intended audience is front-end developers who want gestures, groups and a lightbox without writing the state machine themselves, and the topics attached to the repository name lightbox, photoviewer and photoswipe alongside npm and standalone, which is a fair description of where it sits in the ecosystem.
Groups are reconciled right before a photo opens, not continuously
The delegated listener is only half of the story. The second half is a reconciliation pass, and the README is remarkably precise about when it runs and what it does not cover.
Right before a photo is opened, SmartPhoto reconciles that photo's group against the current DOM. Newly appended matching elements are added, and elements that have since been removed from the DOM are dropped from the group, with the remaining indices recalculated.
The limitations are stated just as precisely, and they are the part to read carefully. Reconciliation only happens right before a photo is opened, whether through a click, through show() or through hash restoration, and not continuously. So if an element disappears from the DOM while the viewer is already open, and you call next() or prev() during that session, the removal is not reflected until the viewer is opened again. That is a real behavioural edge: a gallery that removes thumbnails while the lightbox is open will still show the removed image until the user closes and reopens the viewer.
The second limitation concerns rebuilding a container. The library does not support replacing an entire container's innerHTML, because that recreates existing elements as brand-new DOM nodes and produces duplicate items, since the old and new elements are not recognized as the same one. Only appending or removing individual elements is supported. If you regenerate the whole container, the documented answer is to call destroy() and construct a new instance.
The third is that changing data-group on an element that has already been opened or registered has no effect, because the group is fixed at first registration. Changing it before the element is first interacted with is picked up correctly.
The fourth is a caution about multiple instances. If multiple instances are built with overlapping selectors, you should avoid relying on distinguishing exactly which instance handles a click for elements that could match either.
None of these are bugs to be worked around so much as the boundary of the design, and having them written down is rarer than it should be.
Data source mode for images that come from an API
The second half of the library is the programmatic path, and it exists because not every gallery is in the markup.
Instead of scanning anchor elements in the page, you can pass an array of slide objects directly. The README notes the inspiration, yet-another-react-lightbox, and the case it serves: images that come from an API or a JS-rendered list. The example shows the shape:
const photo = new SmartPhoto([
{ src: "/img/bear-large.jpg", thumb: "/img/bear.jpg", caption: "bear", id: "bear" },
{ src: "/img/camel-large.jpg", thumb: "/img/camel.jpg", caption: "camel", id: "camel", width: 1200, height: 800 },
]);
photo.show(0); // open by index
photo.show("camel"); // or by id
photo.next();
photo.prev();
photo.hide();
photo.on("change", () => { /* ... */ }); // same event contract as HTML modeThe slide fields are a direct translation of the HTML attributes. src is required and is the full-size image URL, equivalent to href in HTML mode. thumb is optional and is the thumbnail URL used in the nav strip, falling back to src. caption is optional and equivalent to data-caption. alt is the image alt text, falling back to caption and then to src. id is the identifier used by show(id) and the URL hash, falling back to the index. group is the group name, equivalent to data-group, falling back to "nogroup". width and height are the natural image size in pixels, and when they are given SmartPhoto skips the preload used to measure the image.
That last field is a performance detail worth noting, because it is the kind of thing that only shows up on a slow connection: supplying the intrinsic size lets the viewer lay out the slide without first loading the image to measure it.
Both modes share the same public API, options and events, which the README states explicitly. The show method also accepts options, with options.group choosing which group to open and options.trigger naming the element to animate from and to return focus to. That last one matters for accessibility, since it means the viewer can hand focus back to the thumbnail the user came from.
Gestures, the accelerometer and keyboard access in one viewer
The feature list is short, and each item is a specific interaction rather than a category.
The gestures are the intuitive ones: pinch-in, pinch-out, drag and swipe. The feature list also mentions using the accelerometer to move images, which the options table backs with two separate switches. useOrientationApi uses the accelerometer, the deviceorientation event, to move a zoomed image, and it is off by default. verticalGravity applies device-tilt gravity to the vertical axis too, in addition to horizontal, and it is also off by default. The separation matters: a user who has enabled orientation control for panning horizontally does not automatically get vertical tilt as well.
Accessibility is a listed feature, with the viewer accessible from keyboards and screen-readers. The README does not spell out the markup contract that this depends on, which is a gap to close by trying it rather than assuming it. The other listed feature, showing pictures via URL hash, is implemented through the useHistoryApi option, which updates the URL hash in the form #group=…&photo=… via the History API, and that option is on by default. The hash is what makes a photo linkable, and it also means the viewer state can be restored, which is the third path that triggers reconciliation.
Photos can make groups, which is what the data-group attribute does in HTML mode, and the README's standalone example shows three anchors with data-group="0" and distinct data-caption and data-id values.
The last option in the visible table is useViewTransitionApi, on by default, which uses the View Transition API for the open animation when the browser supports it and falls back to the clone-based animation when the option is disabled or the browser does not support it. The remaining visible options are arrows for the previous and next arrows, nav for the navigation images at the bottom, and showAnimation for animating the open and close transition, all enabled by default.
That combination of defaults, gestures on, animations on, hash history on, tilt off, is a coherent default for a mobile-first viewer, and each one is a single flag to change.
The build pipeline behind the npm package
The repository is a TypeScript project with a modern, tool-heavy setup, and the package.json describes it in full.
The install is the usual pair of commands, one for npm and one for yarn:
npm install smartphoto --saveyarn add smartphotoThe published package is smartphoto, at version 2.1.18, with a main entry at ./lib/smartphoto.js, a module entry at ./lib/smartphoto.mjs and types at ./lib/types/index.d.ts. The exports map gives bundlers the types, the ES module build for import and the CommonJS build for require, and separately exposes ./css/* so a consumer can import the stylesheet. The files array ships lib, js and css, which is why the README's standalone usage example links a stylesheet and a script from those directories rather than from a CDN bundle.
The build itself is tsup for the JavaScript, tsc against tsconfig.build.json for the types, and a three-step CSS pipeline: clean:css removes and recreates the css directory, css:copy copies styles/smartphoto.css into it, and css:min runs postcss to produce smartphoto.min.css. The main build script chains all three, so the stylesheet and the script are always published in step with the code.
The test setup is split the same way. Unit tests run through vitest, with a watch mode and a coverage variant, and end-to-end tests run through playwright with a playwright.config.mts at the root. The linting is biome, with a check-and-write script for local use and a separate ci mode used in the prepack hook, and there is a lint-staged configuration that runs biome on files under src and test. Husky is wired in through the prepare script, so the git hooks are installed when the package is prepared.
The prepack script is the interesting one, because it is the release gate: lint:ci, then typecheck with tsc --noEmit, then the unit tests, then the build. The release scripts are version bumps with a push, one for patch, one for minor and one for major, so a release runs the full gate before anything is published.
The examples/ directory mirrors the documentation: there are vanilla.html, jquery.html, dynamic.html, event.html, lazy.html, multiple.html, noimage.html, programmatic.html and the resizeFill family, plus an assets/ directory and a src/ directory for the example pages.
The release train, the licence and what to check before shipping
On maintenance, the repository is not archived and the last push was on 2026-10-19, on the master branch. The releases are frequent and versioned in patch: v2.1.18 on 2026-09-29, v2.1.17 on 2026-09-11 and v2.1.16 on 2026-09-10. Two patch releases on consecutive days and a third in the same month, on a 2.1.x line, is the signature of a library that is still being fixed rather than frozen.
The licence is MIT, which the README badge and the LICENSE file at the root of the tree agree on. For a front-end library that is about as low a friction as it gets, and this review does not interpret licence terms.
The public documentation is at appleple.github.io/SmartPhoto, and the README notes a Japanese version of the docs for readers who prefer it, which is a small signal that the project is maintained by someone who works with a Japanese-speaking audience.
What should you check before shipping this into a production site? The first is the reconciliation behaviour, because that is the part that differs most from a naive lightbox and the part with the most surprising edges. If your page replaces a gallery container wholesale, the README says you must call destroy() and construct a new instance, and if you get that wrong you will get duplicate items rather than an error.
The second is accessibility in your own markup. The library lists keyboard and screen reader support as a feature, but the README does not document what it expects of your elements beyond the data attributes in the example, so testing with your actual markup and your own focus management is the only way to know whether it holds up.
The third is the options you do not want. The defaults are generous, with arrows, nav, animations, hash history and the View Transition API all on, and tilt off. If your site already manages scroll locking or history state, useHistoryApi and showAnimation are the two switches most likely to collide with existing behaviour, and both are single flags.
Editorial conclusion
SmartPhoto fits a site whose image list grows as the user scrolls, because the delegated listener and the reconciliation pass mean newly appended anchors are picked up without any extra call, and the data source mode covers the case where images come from an API instead of the markup. It is the wrong choice for a page that regenerates its whole container, since the README states that replacing innerHTML produces duplicate items and that you must call destroy() and construct a new instance instead, and it is the wrong choice if you need to change a group after a photo has already been opened, because the group is fixed at first registration. Before you adopt it, read the reconciliation rules in the README, and check the accessible keyboard and screen reader behaviour with your own markup rather than assuming it, because the library claims keyboard and screen reader support without detailing the markup contract.
Frequently asked questions
How do I install SmartPhoto?
Via npm with `npm install smartphoto --save`, or with yarn using `yarn add smartphoto`. The package is published as smartphoto, with a main build, an ES module build, type declarations and a separate CSS entry point.
Does SmartPhoto pick up images added to the page after it starts?
Yes, when it is constructed with a CSS selector string, because clicks are handled through a single delegated listener. Elements added later by Ajax or infinite scroll are picked up by clicking them, with no need to call addItem() or addNewItem(). When a NodeList or Element[] is passed, or in data source mode, you add and remove items explicitly.
Can I use SmartPhoto with images loaded from an API?
Yes, in data source mode. You construct SmartPhoto with an array of slide objects holding src, and optionally thumb, caption, alt, id, group, width and height, then call show by index or by id. Both modes share the same public API, options and events.
What happens if I replace the gallery container's innerHTML?
The README states that SmartPhoto does not support replacing an entire container's innerHTML, since that recreates existing elements as brand-new DOM nodes and produces duplicate items. Only appending or removing individual elements is supported, and if you regenerate the container you should call destroy() and construct a new instance.
Can I change the data-group of an item after SmartPhoto has opened it?
No. The README says changing data-group on an element that has already been opened or registered has no effect, because the group is fixed at first registration. Changing it before the element is first interacted with is picked up correctly.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/appleple-smartphoto)Was this useful?