SmartPhoto
The most easy to use responsive image viewer especially for mobile devices
See https://appleple.github.io/SmartPhoto/ for complete docs and demos
If you are Japasese, See here https://www.appleple.com/blog/javascript/smartphoto-js.html instead.
Feature
- Intuitive gestures such as pinch-in/pinch-out/drag/swipe
- Use Accelerometer to move images
- Accessible from keyboards and screen-readers
- Show pictures via URL hash
- Can make photo groups
Installation
via npmnpm install smartphoto --save
or yarn
yarn add smartphoto
Usage
requireconst SmartPhoto = require('smartphoto');
smartphoto.js
document.addEventListener('DOMContentLoaded',function(){
new SmartPhoto(".js-smartphoto");
});
jquery-smartphoto.js
$(function(){
$(".js-smartphoto").SmartPhoto();
});
Basic Standalone Usage
<a href="./assets/large-bear.jpg" class="js-smartphoto" data-caption="bear" data-id="bear" data-group="0">
<img src="./assets/bear.jpg" width="360"/>
</a>
<a href="./assets/large-camel.jpg" class="js-smartphoto" data-caption="camel" data-id="camel" data-group="0">
<img src="./assets/camel.jpg" width="360"/>
</a>
<a href="./assets/large-rhinoceros.jpg" class="js-smartphoto" data-caption="rhinoceros" data-id="sai" data-group="0">
<img src="./assets/rhinoceros.jpg" width="360"/>
</a>
<link rel="stylesheet" href="./css/smartphoto.min.css">
<script src="./js/smartphoto.js"></script>
<script>
document.addEventListener('DOMContentLoaded',function(){
new SmartPhoto(".js-smartphoto");
});
</script>
When SmartPhoto is constructed with a CSS selector string (as above), clicks are handled via a single delegated listener, so elements added to the page after construction (e.g. by Ajax/infinite scroll) are picked up automatically just by clicking them — no need to call addItem()/addNewItem() manually. Right before a photo is opened, SmartPhoto also 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 (remaining indices are recalculated). This auto-detection only applies to the selector-string form; when a NodeList/Element[] is passed (or in data source mode below), add/remove items explicitly via addItem()/addNewItem().
A few things to keep in mind:
- Reconciliation only happens right before a photo is opened (via a click,
show(), or hash restoration) — not continuously. If an element disappears from the DOM while the viewer is already open and younext()/prev()through the same session, that removal isn't reflected until the viewer is opened again. - It does not support replacing an entire container's
innerHTML(which recreates existing elements too, as brand-new DOM nodes) — that produces duplicate items, since the old and new elements aren't recognized as the same one. Only appending/removing individual elements is supported; if you regenerate the whole container, calldestroy()and construct a new instance instead. - Changing
data-groupon an element that has already been opened/registered has no effect (the group is fixed at first registration). Changing it before the element is first interacted with is picked up correctly. - If multiple instances are built with overlapping selectors, avoid relying on distinguishing exactly which instance handles a click for elements that could match either.
Programmatic usage (data source mode)
Instead of scanning elements in the page, you can pass an array of slide objects directly (inspired by yet-another-react-lightbox). This is useful when your images come from an API or a JS-rendered list.
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 mode
Slide fields:
| field | required | description |
|---|---|---|
| src | yes | full-size image URL (equivalent to href in HTML mode) |
| thumb | no | thumbnail URL used in the nav strip. Falls back to src |
| caption | no | caption text (equivalent to data-caption) |
| alt | no | image alt text. Falls back to caption, then src |
| id | no | identifier used by show(id) and the URL hash. Falls back to the index |
| group | no | group name (equivalent to data-group). Falls back to "nogroup" |
| width / height | no | natural image size in px. When given, SmartPhoto skips the preload used to measure the image |
show(indexOrId, options) also accepts options.group (which group to open) and options.trigger (the element to animate from / return focus to). Both HTML mode and data source mode share the exact same public API, options, and events.
Option
| variable | description | default |
|---|---|---|
| arrows | prev/next arrows | true |
| nav | navigation images at the bottom | true |
| showAnimation | animate the open/close transition | true |
| verticalGravity | apply device-tilt gravity to the vertical axis too (in addition to horizontal) | false |
| useOrientationApi | use the accelerometer (deviceorientation) to move a zoomed image | false |
| useHistoryApi | update the URL hash (#group=…&photo=…) via the History API |
true |
| useViewTransitionApi | use the View Transition API for the open animation when the browser supports it (falls back to the clone-based animation when disabled or unsupported) | true |
| swipeTopToClose | close the viewer on an upward swipe | false |
| swipeBottomToClose | close the viewer on a downward swipe | true |
| pinchToClose | close the viewer when the photo is pinched in clearly below its base scale (fit/fill) | true |
| swipeOffset | minimum swipe distance (px) to trigger navigation/close | 100 |
| swipeVelocity | minimum swipe speed (px/ms) that triggers navigation even below swipeOffset (fast flicks) | 0.5 |
| headerHeight | height (px) reserved for the header when fitting images | 60 |
| footerHeight | height (px) reserved for the footer when fitting images | 60 |
| resizeStyle | resize images to fill/fit on the screen | 'fit' |
| animationSpeed | animation speed (ms) when switching/opening/closing images | 300 |
| forceInterval | frequency (ms) to apply force to images | 10 |
| registance | friction applied to the inertia scroll of a zoomed image | 0.5 |
| loadOffset | number of neighboring slides to preload around the current one | 2 |
| lazyAttribute | attribute read for a lazy-loaded thumbnail (HTML mode only) | 'data-src' |
| classNames | override any of the generated CSS class names | see source |
| message | override screen-reader text (gotoNextImage / gotoPrevImage / closeDialog / carouselLabel) |
see source |
Hide parts
document.addEventListener('DOMContentLoaded',function(){
new SmartPhoto(".js-smartphoto",{
arrows: false,
nav: false
});
});
Fit/Fill Option
You can choose if you want to scale images to fit/fill
document.addEventListener('DOMContentLoaded',function(){
new SmartPhoto(".js-smartphoto",{
resizeStyle: 'fit'
});
});
Event
// when the modal opened
photo.on('open',function(){
console.log('open');
});
// when the modal closed
photo.on('close',function(){
console.log('close');
});
// when all images are loaded
photo.on('loadall',function(){
console.log('loadall');
});
// when photo is changed
photo.on('change',function(){
console.log('change');
});
// when swipe started
photo.on('swipestart',function(){
console.log('swipestart');
});
// when swipe ended
photo.on('swipeend',function(){
console.log('swipeend');
});
// when zoomed in
photo.on('zoomin',function(){
console.log('zoomin');
});
// when zoomed out
photo.on('zoomout',function(){
console.log('zoomout');
});
Methods
| method | description |
|---|---|
on(event, listener) |
subscribe to one of the events listed above |
destroy() |
remove the viewer and all of its event listeners |
[Symbol.dispose]() |
same as destroy(). Lets a using declaration destroy the instance automatically when it goes out of scope: { using photo = new SmartPhoto(...); } |
gotoSlide(index) |
go to the slide at index within the current group |
hidePhoto(dir?) |
close the viewer. dir is 'bottom' (default) or 'top' and controls the close animation direction |
zoomPhoto() / zoomOutPhoto() |
zoom the current image in/out programmatically |
addNewItem(element) |
register a new <a> thumbnail element (HTML mode). Only needed when constructed with a NodeList/Element[] or otherwise not using a selector string — with a selector string, elements added later are auto-detected on click (see above) |
show(indexOrId?, options?) |
open the viewer, by index or id. Works in both HTML mode and data source mode. options.group picks the group; options.trigger sets the element to animate from and to return focus to |
hide() |
alias of hidePhoto() |
next() / prev() |
go to the next/previous slide. No-op at the start/end of the group |
addItem(slideOrElement) |
add a new item. Accepts a slide object (data source mode, where explicit registration is always required) or an Element (HTML mode, same caveat as addNewItem) |
currentIndex |
(getter) the index currently displayed within its group |
CSS Custom Properties
| property | description | default |
|---|---|---|
| --smartphoto-animation-speed | animation speed when switching/opening/closing images. Overridden per-instance by the animationSpeed JS option |
300ms |
| --smartphoto-animation-function | easing function used for animations | ease-out |
| --smartphoto-backdrop-color | backdrop color when viewing images | rgba(0, 0, 0, 1) |
| --smartphoto-header-color | header color | rgba(0, 0, 0, .2) |
| --smartphoto-arrow-top | vertical position of the prev/next arrows, e.g. move them off the image on narrow screens with a media query | 50% |
Set these on .smartphoto (or :root) to override the defaults, no rebuild required:
.smartphoto {
--smartphoto-animation-speed: 450ms;
--smartphoto-animation-function: ease-in-out;
--smartphoto-backdrop-color: rgba(0, 0, 0, 0.9);
--smartphoto-header-color: rgba(0, 0, 0, 0.4);
}
@media (max-width: 480px) {
.smartphoto {
--smartphoto-arrow-top: 85%;
}
}
The bottom thumbnail nav (.smartphoto-nav) also reserves env(safe-area-inset-bottom) automatically so it isn't hidden behind the iOS home indicator/toolbar.