YurbaPhotoViewer
Fullscreen image viewer with gallery support, navigation, zoom, rotate, download, captions, and customizable corner buttons.
<link rel="stylesheet" href="/dist/yurba-pv.min.css">
<script src="/dist/yurba-pv.min.js"></script>
Create a viewer with YurbaPV.create(). It appends itself to document.body and scans the document for elements with a data-ypv attribute automatically.
const viewer = YurbaPV.create()
Add photos in HTML - no extra JS needed for static content.
<a href="/photo-large.jpg" data-ypv="album" data-id="0" data-caption="Sunset">
<img src="/photo-thumb.jpg">
</a>
Links added dynamically are bound manually with bind().
viewer.bind(document.querySelector('#my-link'))
All options are passed to YurbaPV.create().
const viewer = YurbaPV.create({
icons: {
left: '<span class="material-symbols-rounded">arrow_back</span>',
right: '<span class="material-symbols-rounded">arrow_forward</span>',
zoom: '<span class="material-symbols-rounded">search</span>',
download: '<span class="material-symbols-rounded">download</span>',
rotate: '<span class="material-symbols-rounded">refresh</span>',
},
buttons: [
{
html: '<a class="y-pv__btn" id="delete"><span class="material-symbols-rounded">delete</span></a>',
prepend: false,
onClick: (viewer) => {
viewer.removePhoto(viewer.getCurrentGallery(), viewer.getCurrentPhoto())
viewer.close()
}
}
]
})
| Option | Type | Description |
|---|---|---|
icons optional |
object | Replace built-in icons, see Icons. Each value is an HTML string; keys left out keep the default. |
controls optional |
object | Show or hide individual built-in buttons. Keys: zoom, rotate, download. Set to false to hide. Default: all visible. |
buttons optional |
array | Custom buttons to add to the corner button row. Each item: { html, prepend?, onClick? }. Use the y-pv__btn class to match built-in styling. prepend inserts before the built-ins (default false). onClick(viewer, event) is called on click. |
| Attribute | Type | Description |
|---|---|---|
data-ypv required |
string | Gallery id. Links sharing the same id form one gallery and can be navigated with the arrows. |
data-id required |
number | Numeric position of the photo inside its gallery. Determines order and prev/next. |
href / data-url required |
string | Full-size image URL. href is used first, falling back to data-url. |
data-caption optional |
string | Caption shown over the image. |
data-type optional |
string | video makes the link a video, href being its URL. See Videos. |
data-poster / data-ratio / data-download optional |
string | Video: the image before it plays, its aspect ratio (9 / 16, 16 / 9 by default), and a file to download instead of the one shown (for a photo too). A video without data-download has no download button. |
A gallery can hold videos as well as photos, and the arrows page through both. The viewer draws a video with the first that's available: your own renderVideo(item, container, viewer) from the config, then YurbaVP in classic mode if the page has loaded it (options from config.video), then a plain <video controls>, which plays HLS through hls.js when the page has it. YurbaPV doesn't depend on either library.
A video zooms and pans like a photo, with the wheel or the zoom button; a double click stays with the player and rotation is off. The video is a little shorter than the screen so the caption fits below it. The video stops and frees its stream when you move to another item or close the viewer.
<a href="https://cdn.example.com/clip/master.m3u8" data-ypv="album" data-id="4"
data-type="video" data-poster="/clip.jpg" data-ratio="9 / 16">
<img src="/clip-thumb.jpg">
</a>
// One video from a button, outside any gallery
viewer.showItem({ type: 'video', href: '/clip.mp4', poster: '/clip.jpg', ratio: '16 / 9' })
// The item on screen, with any extra fields it was given
viewer.getCurrentItem() // { type: 'video', href, poster, ratio, caption, ... }
// Drawn your own way
YurbaPV.create({
renderVideo: (item, container) => {
const video = document.createElement('video')
video.src = item.href
video.controls = true
container.appendChild(video)
return { destroy: () => video.pause() }
},
})
Live demo
Click a thumbnail to open the viewer. Use the arrows or ←/→ keys to navigate, the corner buttons to zoom, download or rotate, and Esc or the backdrop to close.
A gallery of photos and videos together: the arrows page through both. With the switch on, videos are drawn by YurbaVP, which this page loads; try holding the video for 2x, or Space, ←/→ and M while it's focused. With it off, the viewer falls back to a plain <video controls>.
// A video outside any gallery, e.g. from a button
viewer.showItem({
type: 'video',
href: '/video/clip.mp4',
ratio: '9 / 16',
caption: 'Opened with showItem()',
})
Controls
Built-in interactions - no configuration needed.
| Input | Action |
|---|---|
← / → or nav arrows | Previous / next photo in the gallery. |
Esc or backdrop click | Close the viewer. |
| Double-click image | Zoom in (first click) / zoom out (second click). |
Scroll the mouse wheel over the viewer to zoom in or out continuously (0.5× - 4×). The zoom button in the corner snaps between 1× and 2×.
When the image is zoomed in beyond the viewport size, click and drag to pan. The cursor changes to a grab hand when panning is available. Zooming back out to fit the screen resets the pan position automatically.
The caption is hidden while the image is zoomed in and reappears when zoom returns to 1×.
The close button, navigation arrows, corner buttons, and caption fade out after 2 seconds of inactivity. Any mouse movement or click resets the timer and brings the UI back.
API
Methods on the <yurba-pv> element.
viewer.bind(element, caption)
| Param | Type | Description |
|---|---|---|
element required |
HTMLElement | A link carrying the data attributes. Registers it into its gallery and opens the viewer on click. |
caption optional |
string | Fallback caption used when the element has no data-caption. |
Elements already in the DOM with data-ypv are bound automatically - call bind() only for content added afterwards.
viewer.open()
viewer.close()
Show or hide the viewer with a fade. open() emits yurba-pv.open, close() emits yurba-pv.close. The viewer also closes on Esc or a backdrop click.
viewer.setPhoto(galleryId, photoId)
Displays a specific photo by gallery and numeric id, updating the caption and prev/next arrows. Called internally on click, but available for manual control.
viewer.removePhoto(galleryId, photoId)
Removes a photo from a gallery. The gallery itself is dropped once empty.
viewer.clearGallery(galleryId)
Empties a gallery. Useful before re-binding a dynamic list of images.
| Method | Returns | Description |
|---|---|---|
getCurrentElement() |
HTMLElement | The link that opened the currently shown photo. |
getCurrentGallery() |
string | Id of the active gallery. |
getCurrentPhoto() |
number | Id of the active photo. |
Events
Both events fire on the <yurba-pv> element and carry currentGallery and currentPhoto properties.
Fired when the viewer opens.
viewer.addEventListener('yurba-pv.open', e => {
console.log(e.currentGallery, e.currentPhoto)
})
Fired when the viewer closes (button, backdrop, or Esc).
viewer.addEventListener('yurba-pv.close', () => {})
Fired whenever another item is shown, on opening too. getCurrentItem() gives it, e.g. to show or hide a custom button for it.
viewer.addEventListener('yurba-pv.change', () => {
report.hidden = !viewer.getCurrentItem()?.report
})
Customization
Control which built-in buttons are visible and add your own buttons to the corner button row.
Pass a controls object to YurbaPV.create(). Each key corresponds to a built-in button - set it to false to hide it. Omitted keys default to visible.
const viewer = YurbaPV.create({
controls: {
zoom: false,
rotate: false,
download: false,
}
})
| Key | Default | Description |
|---|---|---|
zoom |
true | Toggle zoom button. When visible, clicking it zooms in; clicking again resets to 1×. Scroll wheel zoom is always available regardless of this setting. |
rotate |
true | Toggle rotate button. Each click rotates the image 90°. |
download |
true | Toggle download button. Fetches the full-size image and triggers a browser download. |
Pass an icons object to YurbaPV.create(). Each value is HTML inserted as given; keys left out keep the built-in icon. The defaults are in YurbaPV.ICONS.
const viewer = YurbaPV.create({
icons: {
close: '<svg viewBox="0 0 24 24" width="24" height="24">...</svg>',
zoomOut: '<span class="material-symbols-rounded">zoom_out</span>',
}
})
| Key | Default | Description |
|---|---|---|
left | arrow_back | Previous item. |
right | arrow_forward | Next item. |
close | close | Close button. |
zoom | zoom_in | Zoom button. |
zoomOut | zoom | Zoom button while zoomed in. Same as zoom unless set. |
rotate | rotate_right | Rotate button. |
download | download | Download button. |
Add your own buttons to the corner button row via the buttons array. Each entry is an object with an html string and an optional prepend flag. Use the y-pv__btn class to match the built-in button style.
const viewer = YurbaPV.create({
buttons: [
{
html: '<a class="y-pv__btn"><span class="material-symbols-rounded">share</span></a>',
prepend: true,
onClick: (viewer, e) => {
console.log('share', viewer.getCurrentPhoto())
}
},
{
html: '<a class="y-pv__btn"><span class="material-symbols-rounded">delete</span></a>',
onClick: (viewer) => {
viewer.removePhoto(viewer.getCurrentGallery(), viewer.getCurrentPhoto())
viewer.close()
}
}
]
})
| Key | Type | Description |
|---|---|---|
html required |
string | HTML markup inserted into the corner button row. Use the y-pv__btn class to match built-in styling. |
onClick optional |
function | Click handler. Receives (viewer, event) - viewer is the <yurba-pv> element, event is the MouseEvent. |
prepend optional |
boolean | When true, inserts the button before the built-in buttons. Default: false (appends after). |