Photo Grid Gallery
An even, responsive grid of captioned photos with no JavaScript.
6 個のフレームワーク初級
A thumbnail grid whose photos open in a real modal dialog.
<!--
A lightbox is a modal dialog that happens to contain a photo, and it owes the
user everything a dialog owes: role="dialog" + aria-modal, a focus trap,
Escape to close, and focus returned to the thumbnail that opened it.
Skip the last one and a keyboard user is dumped back on <body> - they close
the fourth photo and find themselves at the top of the page. It is the single
most-skipped line in every lightbox on the web.
-->
<div class="lightbox" data-lightbox>
<ul class="lightbox__grid">
<li>
<button class="lightbox__thumb" type="button" aria-haspopup="dialog" data-index="0" data-full="/images/photo-1.jpg" data-alt="Fishing boats moored under an orange sky" data-title="Harbour at dawn">
<img src="/images/thumb-1.jpg" alt="" />
<span class="lightbox__sr">Open Harbour at dawn</span>
</button>
</li>
<li>
<button class="lightbox__thumb" type="button" aria-haspopup="dialog" data-index="1" data-full="/images/photo-2.jpg" data-alt="A footpath switching back through pine forest" data-title="Ridge trail">
<img src="/images/thumb-2.jpg" alt="" />
<span class="lightbox__sr">Open Ridge trail</span>
</button>
</li>
<li>
<button class="lightbox__thumb" type="button" aria-haspopup="dialog" data-index="2" data-full="/images/photo-3.jpg" data-alt="Concrete stairwell seen from directly below" data-title="Stairwell study">
<img src="/images/thumb-3.jpg" alt="" />
<span class="lightbox__sr">Open Stairwell study</span>
</button>
</li>
<li>
<button class="lightbox__thumb" type="button" aria-haspopup="dialog" data-index="3" data-full="/images/photo-4.jpg" data-alt="Long exposure of traffic crossing a bridge at night" data-title="Night crossing">
<img src="/images/thumb-4.jpg" alt="" />
<span class="lightbox__sr">Open Night crossing</span>
</button>
</li>
</ul>
<div class="lightbox__overlay" data-lightbox-overlay hidden>
<div class="lightbox__dialog" role="dialog" aria-modal="true" aria-labelledby="lightbox-caption" data-lightbox-dialog>
<img class="lightbox__image" src="" alt="" data-lightbox-image />
<p class="lightbox__caption" id="lightbox-caption" data-lightbox-caption></p>
<div class="lightbox__actions">
<button class="lightbox__action" type="button" data-lightbox-prev>Previous</button>
<span class="lightbox__counter" data-lightbox-counter aria-hidden="true"></span>
<button class="lightbox__action" type="button" data-lightbox-next>Next</button>
<button class="lightbox__action lightbox__action--close" type="button" data-lightbox-close>Close</button>
</div>
</div>
</div>
</div>
<script>
(function () {
document.querySelectorAll('[data-lightbox]').forEach(function (root) {
var overlay = root.querySelector('[data-lightbox-overlay]');
var dialog = root.querySelector('[data-lightbox-dialog]');
var image = root.querySelector('[data-lightbox-image]');
var caption = root.querySelector('[data-lightbox-caption]');
var counter = root.querySelector('[data-lightbox-counter]');
var closeButton = root.querySelector('[data-lightbox-close]');
var thumbs = Array.prototype.slice.call(root.querySelectorAll('[data-index]'));
var opener = null;
var index = 0;
function show(next) {
index = (next + thumbs.length) % thumbs.length;
var thumb = thumbs[index];
image.src = thumb.dataset.full;
image.alt = thumb.dataset.alt;
caption.textContent = thumb.dataset.title;
counter.textContent = index + 1 + ' / ' + thumbs.length;
}
function open(thumb) {
// Remember WHICH thumb opened us; close() has to hand focus back to it.
opener = thumb;
overlay.hidden = false;
show(Number(thumb.dataset.index));
closeButton.focus();
}
function close() {
overlay.hidden = true;
if (opener) opener.focus();
opener = null;
}
thumbs.forEach(function (thumb) {
thumb.addEventListener('click', function () {
open(thumb);
});
});
closeButton.addEventListener('click', close);
root.querySelector('[data-lightbox-prev]').addEventListener('click', function () {
show(index - 1);
});
root.querySelector('[data-lightbox-next]').addEventListener('click', function () {
show(index + 1);
});
// Backdrop click closes, but only when the backdrop itself is the target
// - without that check a click on the photo bubbles up and closes too.
overlay.addEventListener('click', function (event) {
if (event.target === overlay) close();
});
document.addEventListener('keydown', function (event) {
if (overlay.hidden) return;
if (event.key === 'Escape') {
close();
return;
}
if (event.key !== 'Tab') return;
// The trap. aria-modal hides the page behind from a screen reader, but
// it does nothing to the tab order - without this, Tab walks straight
// out of the dialog and into a page the user cannot see.
var focusable = dialog.querySelectorAll('button');
var first = focusable[0];
var last = focusable[focusable.length - 1];
if (event.shiftKey && document.activeElement === first) {
event.preventDefault();
last.focus();
} else if (!event.shiftKey && document.activeElement === last) {
event.preventDefault();
first.focus();
}
});
});
})();
</script>| Prop | 型 | デフォルト | 説明 |
|---|---|---|---|
items必須 | GalleryPhoto[] | - | レンダリングする項目の配列。 |
className | string | - | ルート要素にマージされる追加クラス。 |
A lightbox is a modal dialog that happens to contain a photo, and it owes the user everything a dialog owes: `role="dialog"` with `aria-modal`, a focus trap, Escape to close, and focus handed back to the thumbnail that opened it. That last one is the most-skipped line in every lightbox on the web - without it a keyboard user closes the fourth photo and lands on `<body>`, back at the top of the page, which is why the component tracks *which* thumb opened it rather than just whether it is open. The trap is genuinely load-bearing too: `aria-modal` hides the page behind from a screen reader but does nothing whatsoever to the tab order, so Tab walks straight out into content the user cannot see. The backdrop closes on click, but only when the backdrop itself is the event target - skip that check and a click on the photo bubbles up and closes the dialog under the user.