Photo grid
For scanning an album of dozens of shots with your eyes until the one you want appears.
Live preview
Source
<section class="dc-pg" aria-label="현장 사진 앨범">
<h2 class="dc-pg__day">9월 11일 목요일 · 사진 9장</h2>
<ul class="dc-pg__grid">
<li class="dc-pg__cell"><button class="dc-pg__tile" type="button"><span class="dc-sr">크게 보기 — 터파기 전경, 오전 8시 12분</span></button></li>
<li class="dc-pg__cell"><button class="dc-pg__tile" type="button"><span class="dc-sr">크게 보기 — 철근 배근, 오전 9시 3분</span></button></li>
<li class="dc-pg__cell"><button class="dc-pg__tile" type="button"><span class="dc-sr">크게 보기 — 거푸집 설치, 오전 10시 5분</span></button></li>
<li class="dc-pg__cell"><button class="dc-pg__tile" type="button"><span class="dc-pg__dur" aria-hidden="true">0:24</span><span class="dc-sr">동영상 재생 — 타설 작업, 24초</span></button></li>
<li class="dc-pg__cell"><button class="dc-pg__tile" type="button"><span class="dc-sr">크게 보기 — 레미콘 반입, 오전 11시 40분</span></button></li>
<li class="dc-pg__cell"><button class="dc-pg__tile" type="button"><span class="dc-sr">크게 보기 — 양생 덮개, 오후 1시 18분</span></button></li>
<li class="dc-pg__cell"><button class="dc-pg__tile" type="button"><span class="dc-sr">크게 보기 — 안전 난간 점검, 오후 2시 30분</span></button></li>
<li class="dc-pg__cell"><button class="dc-pg__tile" type="button"><span class="dc-sr">크게 보기 — 자재 적재 상태, 오후 4시 2분</span></button></li>
<li class="dc-pg__cell"><button class="dc-pg__tile" type="button"><span class="dc-sr">크게 보기 — 퇴근 전 정리, 오후 5시 55분</span></button></li>
</ul>
</section>
.dc-pg {
/* 사진 위에 올라가는 글자와 배지는 두 테마에서 같은 쪽이어야 한다. 스크림이
밝은 모드에서도 어두우므로 글자는 늘 밝은 쪽, 배지 바탕은 늘 어두운 쪽이다.
고정색을 적는 대신 토큰을 양쪽에서 뒤집어 그 두 값을 만든다. */
--dc-on-media: var(--dc-text-on);
--dc-media-ink: var(--dc-text);
font-family: var(--dc-font);
}
@media (prefers-color-scheme: dark) {
.dc-pg {
--dc-on-media: var(--dc-text);
--dc-media-ink: var(--dc-text-on);
}
}
.dc-pg__day {
padding: var(--dc-3) var(--dc-3) var(--dc-2);
font-size: 14px;
font-weight: 700;
letter-spacing: -.01em;
color: var(--dc-text);
}
.dc-pg__grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 2px;
}
.dc-pg__cell { min-width: 0; }
.dc-pg__tile {
position: relative;
display: block;
width: 100%;
aspect-ratio: 1 / 1;
min-height: 44px;
padding: 0;
border: 0;
overflow: hidden;
background: linear-gradient(135deg,
color-mix(in srgb, var(--dc-brand) 30%, var(--dc-sunken)),
var(--dc-sunken));
}
.dc-pg__cell:nth-child(3n + 2) .dc-pg__tile {
background: linear-gradient(135deg,
color-mix(in srgb, var(--dc-info) 32%, var(--dc-sunken)),
var(--dc-sunken));
}
.dc-pg__cell:nth-child(3n) .dc-pg__tile {
background: linear-gradient(135deg,
color-mix(in srgb, var(--dc-ok) 28%, var(--dc-sunken)),
var(--dc-sunken));
}
.dc-pg__cell:nth-child(4n) .dc-pg__tile {
background: linear-gradient(135deg,
color-mix(in srgb, var(--dc-warn) 26%, var(--dc-sunken)),
var(--dc-sunken));
}
.dc-pg__tile:active::before {
content: "";
position: absolute;
inset: 0;
background: color-mix(in srgb, var(--dc-media-ink) 20%, transparent);
}
.dc-pg__dur {
position: absolute;
right: var(--dc-1);
bottom: var(--dc-1);
padding: 0 var(--dc-1);
border-radius: var(--dc-r-xs);
background: color-mix(in srgb, var(--dc-media-ink) 60%, transparent);
color: var(--dc-on-media);
font-size: 11px;
font-weight: 700;
line-height: 1.5;
}
.dc-pg__cell--wide { grid-column: span 2; grid-row: span 2; }
.dc-pg__cell--wide .dc-pg__tile { aspect-ratio: auto; height: 100%; }
.dc-pg__bar {
position: sticky;
top: 0;
z-index: 1;
display: flex;
align-items: center;
justify-content: space-between;
gap: var(--dc-2);
padding: var(--dc-2) var(--dc-2) var(--dc-2) var(--dc-4);
padding-top: calc(var(--dc-safe-top) + var(--dc-2));
background: var(--dc-surface);
border-bottom: 1px solid var(--dc-line);
}
.dc-pg__count {
font-size: 15px;
font-weight: 700;
color: var(--dc-text);
}
.dc-pg__done {
min-width: 44px;
height: 44px;
padding: 0 var(--dc-3);
border: 0;
border-radius: var(--dc-r-sm);
background: transparent;
color: var(--dc-brand-text);
font-size: 15px;
font-weight: 700;
}
.dc-pg__done:active { background: var(--dc-brand-soft); }
.dc-pg__pick {
position: relative;
display: block;
cursor: pointer;
}
.dc-pg__check {
position: absolute;
width: 1px;
height: 1px;
opacity: 0;
appearance: none;
}
.dc-pg__mark {
position: absolute;
top: var(--dc-1);
right: var(--dc-1);
display: grid;
place-items: center;
width: 24px;
height: 24px;
border: 2px solid var(--dc-on-media);
border-radius: var(--dc-r-full);
background: color-mix(in srgb, var(--dc-media-ink) 28%, transparent);
}
.dc-pg__check:checked ~ .dc-pg__mark {
background: var(--dc-brand);
border-color: var(--dc-brand);
}
.dc-pg__check:checked ~ .dc-pg__mark::after {
content: "✓";
color: var(--dc-text-on);
font-size: 13px;
font-weight: 700;
line-height: 1;
}
.dc-pg__check:checked ~ .dc-pg__tile {
outline: 3px solid var(--dc-brand);
outline-offset: -3px;
opacity: .84;
}
.dc-pg__check:focus-visible ~ .dc-pg__tile {
outline: 3px solid var(--dc-brand);
outline-offset: -3px;
}
How to use it
- Three columns on a narrow screen, 2px gaps — wider gaps cost you the photos that fit on one screen
- Bleed edge to edge; in an album, margins take space that photos should have
- Break the run with date headings, or there is no way to tell how far you have looked
- Stamp duration on video and a round check on chosen photos so both read at a glance
Watch out for
- A pinned selection bar needs --dc-safe-top added to its top padding
- Don't signal selection with the check alone — ring the selected tile too, since colour by itself is not a difference
- Tiles announced only as "photo" cannot be picked by ear; put what is in the shot into dc-sr