Media
tier 2 — classesAn image whose size the app does not control, and a gallery of attachments. Long-form text from a source the app did not author is Prose.
Media frame
A bounded box for an image whose size you do not control. The aspect-ratio is
what stops the page reflowing as images arrive — the box has its height before the first
byte — and width/height on the <img> is worth
writing anyway, because it tells the browser the ratio of the file rather than of the frame.
loading="lazy" on anything below the fold; a real alt always, and
<figure class="media-figure"> when there is a caption.
<div class="gallery" style="max-width:560px">
<figure class="media-figure">
<div class="media-frame media-frame--square media-frame--cover">
<img src="https://images.unsplash.com/photo-1587293852726-70cdb56c2866?w=480&h=480&fit=crop&q=70&auto=format"
width="480" height="480" loading="lazy"
alt="A warehouse aisle of tall pallet racking, seen down its length" />
</div>
<figcaption>Square — a thumbnail. Photo by CHUTTERSNAP on Unsplash.</figcaption>
</figure>
<figure class="media-figure">
<div class="media-frame media-frame--cover">
<img src="https://images.unsplash.com/photo-1586528116311-ad8dd3c8310d?w=640&h=400&fit=crop&q=70&auto=format"
width="640" height="400" loading="lazy"
alt="A warehouse floor stacked with yellow crates, staff working in the aisle" />
</div>
<figcaption>16:10 — the default. Photo by Adrian Sulyok on Unsplash.</figcaption>
</figure>
<figure class="media-figure">
<div class="media-frame media-frame--wide media-frame--cover">
<img src="https://images.unsplash.com/photo-1565891741441-64926e441838?w=840&h=360&fit=crop&q=70&auto=format"
width="840" height="360" loading="lazy"
alt="An industrial yard: chevron-marked barriers beside a service road" />
</div>
<figcaption>21:9 — a banner. Photo by Marcin Jozwiak on Unsplash.</figcaption>
</figure>
</div>
Contain or cover
The same photograph in the same square frame, and the whole decision in one pair. The
default contains: the image is letterboxed and all of it is there, which is
what a screenshot, a scanned document or a label needs, because cropping one removes the
part somebody needed. .media-frame--cover fills and crops,
which is what a thumbnail or a banner needs. Picking the wrong one is silent — the image
still appears, just without the corner that mattered.
--cover. The frame is filled and the sides are cropped away — right
for a decorative banner or a thumbnail, and wrong the moment the edges carry
information.
<div class="gallery" style="max-width:560px">
<figure class="media-figure">
<div class="media-frame media-frame--square">
<img src="https://images.unsplash.com/photo-1553413077-190dd305871c?w=640&h=400&fit=crop&q=70&auto=format"
width="640" height="400" loading="lazy"
alt="A long warehouse aisle between tall racks of stored goods" />
</div>
<figcaption>
The default. The whole photograph is there, letterboxed against the frame's own
surface — right for a document, a label, a screenshot, anything a reader has to
be able to read all of. Photo by Ruchindra Gunasekara on Unsplash.
</figcaption>
</figure>
<figure class="media-figure">
<div class="media-frame media-frame--square media-frame--cover">
<img src="https://images.unsplash.com/photo-1553413077-190dd305871c?w=640&h=400&fit=crop&q=70&auto=format"
width="640" height="400" loading="lazy"
alt="The same warehouse aisle, cropped to a square" />
</div>
<figcaption>
<code>--cover</code>. The frame is filled and the sides are cropped away — right
for a decorative banner or a thumbnail, and wrong the moment the edges carry
information.
</figcaption>
</figure>
</div>
A gallery of attachments
.gallery is a breakpoint-free grid of thumbnails. Each tile is a real
<button> or <a> directly inside it — never a
<div> with a click handler — and every frame is square whatever aspect
modifier it carries, so a row of mixed attachments lines up. Which is the point of the mix
here: two photographs somebody took at the dock and two files, in one grid, squared by the
same rule.
Photographs by Arno Senoner and Hannes Egler on Unsplash.
<div class="gallery" style="max-width:520px">
<button class="btn-ghost" type="button" style="padding:0; text-align:start">
<span class="media-frame media-frame--cover">
<img src="https://images.unsplash.com/photo-1592085198739-ffcad7f36b54?w=280&h=280&fit=crop&q=70&auto=format"
width="280" height="280" loading="lazy"
alt="Photo of the bay: blue steel racking uprights and beams, seen close up" />
</span>
<span class="text-sm text-muted text-truncate sedna-mt-1">racking-bay-4209.jpg</span>
</button>
<button class="btn-ghost" type="button" style="padding:0; text-align:start">
<span class="media-frame media-frame--cover">
<img src="https://images.unsplash.com/photo-1504376830547-506dedfe1fe9?w=280&h=280&fit=crop&q=70&auto=format"
width="280" height="280" loading="lazy"
alt="Photo of the dock: hazard-striped loading bays and a red line marked on the floor" />
</span>
<span class="text-sm text-muted text-truncate sedna-mt-1">dock-bays-4209.jpg</span>
</button>
<button class="btn-ghost" type="button" style="padding:0; text-align:start">
<span class="media-frame">
<i class="ri-file-pdf-2-line" style="font-size:26px; color:var(--border-hover)"></i>
</span>
<span class="text-sm text-muted text-truncate sedna-mt-1">pod-ord-4209.pdf</span>
</button>
<button class="btn-ghost" type="button" style="padding:0; text-align:start">
<span class="media-frame">
<i class="ri-file-excel-2-line" style="font-size:26px; color:var(--border-hover)"></i>
</span>
<span class="text-sm text-muted text-truncate sedna-mt-1">stock-check.xlsx</span>
</button>
</div>
<p class="form-hint" style="max-width:520px">
Photographs by Arno Senoner and Hannes Egler on Unsplash.
</p>
Pictures sharing one frame
.gallery--tight packs pictures that belong to one thing — the photos in a
chat message, the ones on a card — two across with a hairline between them. The frame
around them has the corners, so the pictures have none. Three leave a slot; four fill it.
Photos from the dock
Photographs by CHUTTERSNAP and Adrian Sulyok on Unsplash.
<div class="card" style="max-width:360px">
<div class="card-head"><h3>Photos from the dock</h3></div>
<div class="card-body">
<div class="gallery gallery--tight">
<button class="media-open" type="button" aria-label="Open the photo of the racking">
<span class="media-frame media-frame--square media-frame--cover">
<img src="https://images.unsplash.com/photo-1587293852726-70cdb56c2866?w=280&h=280&fit=crop&q=70&auto=format"
width="280" height="280" loading="lazy" alt="A warehouse aisle of tall pallet racking" />
</span>
</button>
<button class="media-open" type="button" aria-label="Open the photo of the crates">
<span class="media-frame media-frame--square media-frame--cover">
<img src="https://images.unsplash.com/photo-1586528116311-ad8dd3c8310d?w=280&h=280&fit=crop&q=70&auto=format"
width="280" height="280" loading="lazy" alt="A warehouse floor stacked with yellow crates" />
</span>
</button>
</div>
<p class="form-hint">Photographs by CHUTTERSNAP and Adrian Sulyok on Unsplash.</p>
</div>
</div>
A link preview
.link-preview is a link unfurled: the picture, then the title, a line and the
host in .link-preview-body. The app fetches the preview and decides whether to
show one; the picture's alt is empty because the title already names the link.
A chat message carries the same card.
The dock schedule for week 38 is up, with the Thursday cut-off moved to 14:00.
Photograph by Adrian Sulyok on Unsplash.
<div class="card" style="max-width:520px">
<div class="card-body sedna-col sedna-gap-3">
<p>The dock schedule for week 38 is up, with the Thursday cut-off moved to 14:00.</p>
<a class="link-preview" href="#">
<img src="https://images.unsplash.com/photo-1586528116311-ad8dd3c8310d?w=176&h=176&fit=crop&q=70&auto=format" width="88" height="88" alt="" loading="lazy" />
<span class="link-preview-body">
<strong>Dock schedule — week 38</strong>
<span>Inbound and outbound slots for all four docks, with the Thursday cut-off marked.</span>
<small>ops.example.com</small>
</span>
</a>
<p class="form-hint">Photograph by Adrian Sulyok on Unsplash.</p>
</div>
</div>
Looking at one large
Live — open a picture. .lightbox is a
<dialog> opened with showModal() —
ShowModalAsync from C# — so the top layer, the
focus trap, Escape and the inert page behind it are the platform's. The
.lightbox-stage holds the picture as large as the viewport allows;
.lightbox-head carries the .lightbox-title, the
.lightbox-meta and the .lightbox-actions — download, open, close
— and .lightbox-prev / .lightbox-next go to the neighbours,
with the .lightbox-caption under it all. Which picture is in it is the app's
state, and so are the arrow keys: one dialog whose stage the app re-renders, or one per
attachment as here. A picture that opens one is a .media-open button.
<div class="gallery" style="max-width:520px">
<button class="btn-ghost" type="button" style="padding:0; text-align:start"
onclick="document.getElementById('lb-racking').showModal()">
<span class="media-frame media-frame--cover">
<img src="https://images.unsplash.com/photo-1587293852726-70cdb56c2866?w=280&h=280&fit=crop&q=70&auto=format"
width="280" height="280" loading="lazy"
alt="A warehouse aisle of tall pallet racking, seen down its length" />
</span>
<span class="text-sm text-muted text-truncate sedna-mt-1">racking-bay-4209.jpg</span>
</button>
<button class="btn-ghost" type="button" style="padding:0; text-align:start"
onclick="document.getElementById('lb-dock').showModal()">
<span class="media-frame media-frame--cover">
<img src="https://images.unsplash.com/photo-1586528116311-ad8dd3c8310d?w=280&h=280&fit=crop&q=70&auto=format"
width="280" height="280" loading="lazy"
alt="A warehouse floor stacked with yellow crates, staff working in the aisle" />
</span>
<span class="text-sm text-muted text-truncate sedna-mt-1">crates-4209.jpg</span>
</button>
</div>
<dialog class="lightbox" id="lb-racking" aria-labelledby="lb-racking-title">
<div class="lightbox-head">
<strong class="lightbox-title" id="lb-racking-title">racking-bay-4209.jpg</strong>
<span class="lightbox-meta">1 of 2 · 1.2 MB</span>
<div class="lightbox-actions">
<a class="btn btn-ghost btn-icon" href="https://images.unsplash.com/photo-1587293852726-70cdb56c2866?w=1600&q=80&auto=format" target="_blank" rel="noopener" aria-label="Open the original in a new tab"><i class="ri-external-link-line"></i></a>
<form method="dialog"><button class="btn btn-ghost btn-icon" aria-label="Close"><i class="ri-close-line"></i></button></form>
</div>
</div>
<div class="lightbox-stage">
<img src="https://images.unsplash.com/photo-1587293852726-70cdb56c2866?w=1600&q=80&auto=format"
alt="A warehouse aisle of tall pallet racking, seen down its length" />
</div>
<button class="lightbox-prev" type="button" aria-label="Previous" disabled><i class="ri-arrow-left-s-line"></i></button>
<button class="lightbox-next" type="button" aria-label="Next"
onclick="document.getElementById('lb-racking').close(); document.getElementById('lb-dock').showModal()"><i class="ri-arrow-right-s-line"></i></button>
<p class="lightbox-caption">Bay 4 after the racking was moved. Photo by CHUTTERSNAP on Unsplash.</p>
</dialog>
<dialog class="lightbox" id="lb-dock" aria-labelledby="lb-dock-title">
<div class="lightbox-head">
<strong class="lightbox-title" id="lb-dock-title">crates-4209.jpg</strong>
<span class="lightbox-meta">2 of 2 · 940 KB</span>
<div class="lightbox-actions">
<a class="btn btn-ghost btn-icon" href="https://images.unsplash.com/photo-1586528116311-ad8dd3c8310d?w=1600&q=80&auto=format" target="_blank" rel="noopener" aria-label="Open the original in a new tab"><i class="ri-external-link-line"></i></a>
<form method="dialog"><button class="btn btn-ghost btn-icon" aria-label="Close"><i class="ri-close-line"></i></button></form>
</div>
</div>
<div class="lightbox-stage">
<img src="https://images.unsplash.com/photo-1586528116311-ad8dd3c8310d?w=1600&q=80&auto=format"
alt="A warehouse floor stacked with yellow crates, staff working in the aisle" />
</div>
<button class="lightbox-prev" type="button" aria-label="Previous"
onclick="document.getElementById('lb-dock').close(); document.getElementById('lb-racking').showModal()"><i class="ri-arrow-left-s-line"></i></button>
<button class="lightbox-next" type="button" aria-label="Next" disabled><i class="ri-arrow-right-s-line"></i></button>
<p class="lightbox-caption">The crates on the floor before the belt. Photo by Adrian Sulyok on Unsplash.</p>
</dialog>
A document in it
A PDF or a text file is a .lightbox-frame in the stage — an
<object> with a download link as its fallback, or an
<iframe> with a title — and the download link in
the actions is the way to keep it. Name the frame the way a screen reader would want the
tab it replaces named.
<dialog class="lightbox" id="lb-pod" aria-labelledby="lb-pod-title">
<div class="lightbox-head">
<strong class="lightbox-title" id="lb-pod-title">pod-ord-4209.pdf</strong>
<span class="lightbox-meta">84 KB</span>
<div class="lightbox-actions">
<a class="btn btn-ghost btn-icon" href="…" download aria-label="Download"><i class="ri-download-2-line"></i></a>
<form method="dialog"><button class="btn btn-ghost btn-icon" aria-label="Close"><i class="ri-close-line"></i></button></form>
</div>
</div>
<div class="lightbox-stage">
<object class="lightbox-frame" data="…" type="application/pdf" aria-label="Proof of delivery for ORD-4209">
<a href="…" download>The browser cannot show this PDF here — download it.</a>
</object>
</div>
</dialog>