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=360&fit=crop&q=70&auto=format"
width="640" height="360" loading="lazy"
alt="A warehouse floor stacked with yellow crates, staff working in the aisle" />
</div>
<figcaption>16:9 — 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>
Any ratio, one token
Every frame that holds a picture at a fixed shape reads --media-ratio: the
.media-frame, the .skeleton-image it replaces, a
.choice-tile-art, a
.file-tile-thumb and a
.media-player's video. Each is 16:9 unless it says otherwise
— a file tile is square — so a skeleton swaps for its picture without a jump. Set
the token on one frame or on a whole container: 4 / 3, 3 / 2,
9 / 16. --square and --wide set it too.
.skeleton-image is 16:9, like the frame it turns into.--media-ratio: 4 / 3 on the figure. Any frame inside follows it.<div class="sedna-grid sedna-grid--sm sedna-grid--fit" style="max-width:640px">
<figure class="media-figure">
<div class="skeleton skeleton-image" aria-hidden="true"></div>
<figcaption>Loading. A <code>.skeleton-image</code> is 16:9, like the frame it turns into.</figcaption>
</figure>
<figure class="media-figure">
<div class="media-frame">
<img src="https://images.unsplash.com/photo-1553413077-190dd305871c?w=640&h=360&fit=crop&q=70&auto=format"
width="640" height="360" loading="lazy"
alt="A long warehouse aisle between tall racks of stored goods" />
</div>
<figcaption>Loaded, at the same height. Photo by Ruchindra Gunasekara on Unsplash.</figcaption>
</figure>
<figure class="media-figure" style="--media-ratio: 4 / 3">
<div class="media-frame">
<img src="https://images.unsplash.com/photo-1553413077-190dd305871c?w=640&h=480&fit=crop&q=70&auto=format"
width="640" height="480" loading="lazy"
alt="The same aisle in a 4:3 frame" />
</div>
<figcaption><code>--media-ratio: 4 / 3</code> on the figure. Any frame inside follows it.</figcaption>
</figure>
</div>
Seeing transparency
.media-frame--transparency paints a checkerboard behind the picture, so a
transparent background can be told from a white or grey one — a logo, an icon, a
cut-out, an upload being checked. It is behind the picture alone, so a letterbox stays plain.
The squares are --surface-soft and
--border, so they follow the theme; --checker-size is one square.
<div class="sedna-grid sedna-grid--sm sedna-grid--fit" style="max-width:640px">
<figure class="media-figure">
<div class="media-frame media-frame--transparency">
<svg width="160" height="90" viewBox="0 0 160 90" role="img" aria-label="Logo, transparent background">
<circle cx="45" cy="45" r="28" fill="currentColor" style="color: var(--brand)" />
<rect x="84" y="31" width="52" height="28" rx="6" fill="currentColor" style="color: var(--info-fg)" />
</svg>
</div>
<figcaption>A transparent background: the squares show through around the mark.</figcaption>
</figure>
<figure class="media-figure">
<div class="media-frame media-frame--transparency">
<svg width="160" height="90" viewBox="0 0 160 90" role="img" aria-label="Logo, white background">
<rect width="160" height="90" fill="white" />
<circle cx="45" cy="45" r="28" fill="currentColor" style="color: var(--brand)" />
<rect x="84" y="31" width="52" height="28" rx="6" fill="currentColor" style="color: var(--info-fg)" />
</svg>
</div>
<figcaption>The same mark on a baked-in white box, which the squares give away.</figcaption>
</figure>
</div>
Transparency in every frame
Every frame that shows a picture takes the same modifier: .file-tile-thumb--transparency
on a file tile, .file-thumb--transparency on an attachment waiting to upload, and
.choice-tile-art--transparency on a choice between pictures — where telling a
transparent logo from a boxed one is the whole decision. The lightbox's stage takes
.lightbox-stage--transparency, below.
<div class="sedna-col sedna-gap-4" style="max-width:640px">
<ul class="file-grid" aria-label="Brand files">
<li class="file-tile">
<a class="file-tile-open" href="#">
<span class="file-tile-thumb file-tile-thumb--transparency">
<svg width="160" height="90" viewBox="0 0 160 90" role="img" aria-label="logo-mark.svg">
<circle cx="45" cy="45" r="28" fill="currentColor" style="color: var(--brand)" />
<rect x="84" y="31" width="52" height="28" rx="6" fill="currentColor" style="color: var(--info-fg)" />
</svg>
</span>
<span class="file-name">logo-mark.svg</span>
</a>
<span class="file-meta"><span>4 KB</span></span>
</li>
</ul>
<div class="sedna-row-wrap sedna-gap-2" aria-label="Attached, not yet uploaded">
<span class="file-thumb file-thumb--transparency">
<svg width="48" height="48" viewBox="0 0 48 48" role="img" aria-label="badge.svg">
<circle cx="24" cy="24" r="14" fill="currentColor" style="color: var(--brand)" />
</svg>
<button class="chip-dismiss" type="button" aria-label="Remove badge.svg"><i class="ri-close-line"></i></button>
</span>
</div>
<div class="choice-tiles" role="radiogroup" aria-label="Logo for the invoice header">
<label class="choice-tile">
<input type="radio" name="invoice-logo" value="mark" checked />
<span class="choice-tile-art choice-tile-art--transparency">
<svg width="160" height="90" viewBox="0 0 160 90" role="img" aria-label="Mark alone">
<circle cx="45" cy="45" r="28" fill="currentColor" style="color: var(--brand)" />
<rect x="84" y="31" width="52" height="28" rx="6" fill="currentColor" style="color: var(--info-fg)" />
</svg>
</span>
<span class="choice-tile-title">Mark alone</span>
<span class="choice-tile-hint">Transparent, sits on any paper</span>
</label>
<label class="choice-tile">
<input type="radio" name="invoice-logo" value="boxed" />
<span class="choice-tile-art choice-tile-art--transparency">
<svg width="160" height="90" viewBox="0 0 160 90" role="img" aria-label="Mark on a box">
<rect width="160" height="90" rx="8" fill="currentColor" style="color: var(--surface-strong)" />
<circle cx="45" cy="45" r="28" fill="currentColor" style="color: var(--brand)" />
<rect x="84" y="31" width="52" height="28" rx="6" fill="currentColor" style="color: var(--info-fg)" />
</svg>
</span>
<span class="choice-tile-title">Mark on a box</span>
<span class="choice-tile-hint">Carries its own background</span>
</label>
</div>
</div>
Transparency in the lightbox
The same modifier on the stage of a .lightbox, for a picture opened full size.
<dialog class="lightbox" id="lb-logo" aria-labelledby="lb-logo-title">
<div class="lightbox-head">
<strong class="lightbox-title" id="lb-logo-title">logo-mark.svg</strong>
<div class="lightbox-actions">
<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 lightbox-stage--transparency">
<svg width="480" height="270" viewBox="0 0 160 90" role="img" aria-label="The logo mark on a transparent background">
<circle cx="45" cy="45" r="28" fill="currentColor" style="color: var(--brand)" />
<rect x="84" y="31" width="52" height="28" rx="6" fill="currentColor" style="color: var(--info-fg)" />
</svg>
</div>
</dialog>
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>