Photo Grid Gallery
An even, responsive grid of captioned photos with no JavaScript.
6 个框架初级
A photo stage with prev/next arrows, a position counter and a scrolling thumbnail filmstrip.
<!--
A stage plus a SCROLLING filmstrip - the shape you want once there are more
thumbs than fit on one line, where a fixed strip would either wrap into a
second row or shrink every thumb into uselessness.
Two pieces earn their keep beyond the plain thumbnail carousel:
the strip is a scroll-snap region, so the active thumb can be scrolled into
view as the user pages through it with the arrows; and the counter states
position in words, because "3 of 6" is information a highlighted thumb only
conveys to someone who can see the whole strip at once.
-->
<div class="filmstrip" data-filmstrip>
<section class="filmstrip__stage" aria-roledescription="carousel" aria-label="Photo gallery">
<div class="filmstrip__viewport" aria-live="polite">
<img class="filmstrip__image" src="/images/photo-1.jpg" alt="Fishing boats moored under an orange sky" data-filmstrip-image />
</div>
<div class="filmstrip__bar">
<button class="filmstrip__nav" type="button" aria-label="Previous photo" data-filmstrip-prev>‹</button>
<p class="filmstrip__meta">
<span class="filmstrip__title" data-filmstrip-title>Harbour at dawn</span>
<span class="filmstrip__counter" data-filmstrip-counter>1 of 6</span>
</p>
<button class="filmstrip__nav" type="button" aria-label="Next photo" data-filmstrip-next>›</button>
</div>
</section>
<ul class="filmstrip__strip" data-filmstrip-strip>
<li>
<button class="filmstrip__thumb" type="button" aria-current="true" 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="filmstrip__sr">Show Harbour at dawn</span>
</button>
</li>
<li>
<button class="filmstrip__thumb" type="button" 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="filmstrip__sr">Show Ridge trail</span>
</button>
</li>
<li>
<button class="filmstrip__thumb" type="button" 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="filmstrip__sr">Show Stairwell study</span>
</button>
</li>
<li>
<button class="filmstrip__thumb" type="button" 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="filmstrip__sr">Show Night crossing</span>
</button>
</li>
<li>
<button class="filmstrip__thumb" type="button" data-index="4" data-full="/images/photo-5.jpg" data-alt="Salt flats meeting a pale horizon" data-title="Salt flats">
<img src="/images/thumb-5.jpg" alt="" />
<span class="filmstrip__sr">Show Salt flats</span>
</button>
</li>
<li>
<button class="filmstrip__thumb" type="button" data-index="5" data-full="/images/photo-6.jpg" data-alt="Market awnings in bright primary colours" data-title="Market row">
<img src="/images/thumb-6.jpg" alt="" />
<span class="filmstrip__sr">Show Market row</span>
</button>
</li>
</ul>
</div>
<script>
(function () {
document.querySelectorAll('[data-filmstrip]').forEach(function (root) {
var image = root.querySelector('[data-filmstrip-image]');
var title = root.querySelector('[data-filmstrip-title]');
var counter = root.querySelector('[data-filmstrip-counter]');
var thumbs = Array.prototype.slice.call(root.querySelectorAll('[data-index]'));
var index = 0;
function select(next) {
index = (next + thumbs.length) % thumbs.length;
var thumb = thumbs[index];
image.src = thumb.dataset.full;
// The alt travels with the src so the live region announces WHICH
// photo landed, not just that something changed.
image.alt = thumb.dataset.alt;
title.textContent = thumb.dataset.title;
counter.textContent = index + 1 + ' of ' + thumbs.length;
thumbs.forEach(function (other) {
if (other === thumb) other.setAttribute('aria-current', 'true');
else other.removeAttribute('aria-current');
});
// block:'nearest' matters - without it the browser is free to scroll
// the PAGE vertically to bring the strip into view, yanking the layout
// out from under someone who only pressed Next.
thumb.scrollIntoView({ inline: 'nearest', block: 'nearest', behavior: 'smooth' });
}
thumbs.forEach(function (thumb) {
thumb.addEventListener('click', function () {
select(Number(thumb.dataset.index));
});
});
root.querySelector('[data-filmstrip-prev]').addEventListener('click', function () {
select(index - 1);
});
root.querySelector('[data-filmstrip-next]').addEventListener('click', function () {
select(index + 1);
});
});
})();
</script>| Prop | 类型 | 默认值 | 说明 |
|---|---|---|---|
items必填 | GalleryPhoto[] | - | 要渲染的项目数组。 |
onSelect | (index: number) => void | - | 用户选择菜单项时触发,参数为所选项。 |
className | string | - | 合并到根元素上的额外类名。 |
The shape to reach for once there are more thumbnails than fit on one line - where a fixed strip either wraps into a second row or shrinks every thumb into uselessness. The strip is an `overflow-x-auto` scroll-snap region, so it stays one line however many photos arrive, and selecting a photo scrolls its thumb into view. Note `block: 'nearest'` on that `scrollIntoView`: without it the browser is free to scroll the whole *page* vertically to bring the strip into frame, yanking the layout out from under someone who only pressed Next. The scroll call lives inside the click handler rather than an effect for the same reason - it can then only ever run because a user asked for it, never on mount. The counter states position in words ("3 of 6") because a highlighted thumb only conveys that to someone who can see the entire strip at once, and selection shows as a ring *and* full opacity - two signals, never colour alone.