Skip to content

COMPONENT

Frame

A real image, held at a ratio. It is the counterpart to the placeholder and deliberately the same box, so a layout you reviewed against stand-ins does not move when the photographs arrive.

One source, two ratios

The same 16px square file at 16:9 and 4:5. Only --ratio changed. The source is a flat swatch of the placeholder's tint, so what you are looking at is the box.

FIG 0116:9. A 16px square source, filling a wide box.
FIG 02The same file at 4:5. Only --ratio changed.
HTML
<!-- The source is a 16px square of the placeholder's own tint, inlined so the
     book ships no binaries. Any <img> goes here; this one is flat so the box,
     not the picture, is what you are looking at. -->
<div style="display:grid; grid-template-columns:repeat(auto-fit,minmax(min(100%,240px),1fr)); gap:20px; width:100%">
  <figure style="margin:0; display:flex; flex-direction:column; gap:14px">
    <div class="frame" style="--ratio:16/9">
      <img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAAAFklEQVR42mMQV9ElCTGMahjVMHw1AAA2YWgBpC4dPQAAAABJRU5ErkJggg==" alt="">
    </div>
    <figcaption class="placeholder-caption">
      <span class="placeholder-caption-index">FIG 01</span>16:9. A 16px square source, filling a wide box.
    </figcaption>
  </figure>
  <figure style="margin:0; display:flex; flex-direction:column; gap:14px">
    <div class="frame" style="--ratio:4/5">
      <img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAAAFklEQVR42mMQV9ElCTGMahjVMHw1AAA2YWgBpC4dPQAAAABJRU5ErkJggg==" alt="">
    </div>
    <figcaption class="placeholder-caption">
      <span class="placeholder-caption-index">FIG 02</span>The same file at 4:5. Only <code>--ratio</code> changed.
    </figcaption>
  </figure>
</div>

Before and after the asset arrives

Placeholder and frame, side by side at one ratio. Same hairline, same box, same footprint β€” which is the claim this page exists to make.

PLACEHOLDER Editor, mid-stroke β€” 16:9
BEFOREThe asset has not arrived. The box is designed, not missing.
AFTERThe asset arrived. Same border, same ratio, same footprint β€” nothing around it moved.
HTML
<div style="display:grid; grid-template-columns:repeat(auto-fit,minmax(min(100%,240px),1fr)); gap:20px; width:100%">
  <figure style="margin:0; display:flex; flex-direction:column; gap:14px">
    <div class="placeholder" style="--ratio:16/9">
      <span class="placeholder-kicker">PLACEHOLDER</span>
      <span class="placeholder-note">Editor, mid-stroke β€” 16:9</span>
    </div>
    <figcaption class="placeholder-caption">
      <span class="placeholder-caption-index">BEFORE</span>The asset has not arrived. The box is designed, not missing.
    </figcaption>
  </figure>
  <figure style="margin:0; display:flex; flex-direction:column; gap:14px">
    <div class="frame" style="--ratio:16/9">
      <img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAAAFklEQVR42mMQV9ElCTGMahjVMHw1AAA2YWgBpC4dPQAAAABJRU5ErkJggg==" alt="">
    </div>
    <figcaption class="placeholder-caption">
      <span class="placeholder-caption-index">AFTER</span>The asset arrived. Same border, same ratio, same footprint β€” nothing around it moved.
    </figcaption>
  </figure>
</div>

A drawing is not a photograph

One wide diagram in a square box, twice. The default crops to fill and takes both ends with it; frame-contain shows the whole thing and the frame's tint becomes the mat around it.

FIG 01The default. It crops to fill, and both ends of the drawing are gone.
FIG 02frame-contain. Same file, same box, whole picture, and the tint is now the mat.
HTML
<!-- One wide drawing, twice, in a square box. The source is inlined so the book
     ships no binaries: a 3:1 diagram whose two ends are the whole point.

     BASE64 RATHER THAN A PERCENT-ENCODED SVG, and that is not a preference. An
     SVG written inline into src="…" carries double quotes of its own, and the
     first one closes the attribute β€” the picture silently does not load and the
     box looks empty but correct. -->
<div style="display:grid; grid-template-columns:repeat(auto-fit,minmax(min(100%,240px),1fr)); gap:20px; width:100%">
  <figure style="margin:0; display:flex; flex-direction:column; gap:14px">
    <div class="frame" style="--ratio:1/1">
      <img src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxMjAgNDAiPjxnIGZpbGw9Im5vbmUiIHN0cm9rZT0iIzZiOGVhMyIgc3Ryb2tlLXdpZHRoPSIzIiBzdHJva2UtbGluZWNhcD0icm91bmQiPjxyZWN0IHg9IjIuNSIgeT0iMi41IiB3aWR0aD0iMTE1IiBoZWlnaHQ9IjM1Ii8+PGNpcmNsZSBjeD0iMjAiIGN5PSIyMCIgcj0iOSIvPjxjaXJjbGUgY3g9IjEwMCIgY3k9IjIwIiByPSI5Ii8+PHBhdGggZD0iTTMxIDIwSDg5Ii8+PHBhdGggZD0iTTYwIDh2MjQiLz48L2c+PC9zdmc+" alt="">
    </div>
    <figcaption class="placeholder-caption">
      <span class="placeholder-caption-index">FIG 01</span>The default. It crops to fill, and both ends of the drawing are gone.
    </figcaption>
  </figure>
  <figure style="margin:0; display:flex; flex-direction:column; gap:14px">
    <div class="frame frame-contain" style="--ratio:1/1">
      <img src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxMjAgNDAiPjxnIGZpbGw9Im5vbmUiIHN0cm9rZT0iIzZiOGVhMyIgc3Ryb2tlLXdpZHRoPSIzIiBzdHJva2UtbGluZWNhcD0icm91bmQiPjxyZWN0IHg9IjIuNSIgeT0iMi41IiB3aWR0aD0iMTE1IiBoZWlnaHQ9IjM1Ii8+PGNpcmNsZSBjeD0iMjAiIGN5PSIyMCIgcj0iOSIvPjxjaXJjbGUgY3g9IjEwMCIgY3k9IjIwIiByPSI5Ii8+PHBhdGggZD0iTTMxIDIwSDg5Ii8+PHBhdGggZD0iTTYwIDh2MjQiLz48L2c+PC9zdmc+" alt="">
    </div>
    <figcaption class="placeholder-caption">
      <span class="placeholder-caption-index">FIG 02</span><code>frame-contain</code>. Same file, same box, whole picture, and the tint is now the mat.
    </figcaption>
  </figure>
</div>

A picture that is a link

Press either one. The whole box is the target β€” a reader who wants the project aims at the picture, not at the title beside it.

A dog in profile against a deep blue sky, sitting in tall meadow grass.
HTML
<!-- A picture that is a link to whatever it is a picture of. The anchor covers
     the frame rather than wrapping it, and carries the name β€” a link whose
     content is nothing announces as nothing. -->
<div class="frame" style="--ratio:16/9; width:min(100%,260px)">
  <img src="/gallery/rico-15-thumb.webp"
       alt="A dog in profile against a deep blue sky, sitting in tall meadow grass." />
  <a class="frame-link" href="/primitives/placeholder/" aria-label="Rico in the meadow"></a>
</div>

<!-- Over a clip, the link goes AFTER the video and BEFORE the control. `clip`
     makes the frame a stacking context, so document order alone decides what is
     on top β€” put the link last and it swallows the pause button.

     There is no `controls` attribute here, and that is the trade: with no
     script the browser's own play button would sit under the link. Media that
     navigates does not also play. -->
<div class="frame clip" style="--ratio:16/9; width:min(100%,260px)" x-ds-clip
     data-label-play="Play the clip" data-label-pause="Pause the clip">
  <video muted loop playsinline preload="none" poster="/gallery/rico-17-thumb.avif">
    <source src="/gallery/rico-17-thumb.webm" type="video/webm" />
    <source src="/gallery/rico-17-thumb.mp4" type="video/mp4" />
  </video>
  <a class="frame-link" href="/components/clip/" aria-label="A clip of Rico"></a>
  <button class="clip-toggle" type="button" hidden></button>
</div>

Reference

Read from components/frame.css when this page was built, so it cannot fall behind the stylesheet.

CLASSES.frame.frame-bare.frame-contain.frame-link
READS--ds-focus-ring-inset--ds-focus-width--ds-hairline--ds-radius-frame--ds-rule--ds-tint-cool

Notes

CROPPING IS RIGHT FOR A PHOTOGRAPH AND WRONG FOR A DRAWING

A photograph is a window onto something larger, so cutting the overflow is honest: there is more scene out there either way, and a row of differently-sized sources ends up looking deliberate.

A drawing has no outside. A diagram, a logo, an export from a modeller β€” the edges of the artwork are the artwork, and cropping removes the picture rather than reframing it. That is two kinds of picture, not one kind with a bad crop, which is why the answer is frame-contain and not a better file.

πŸ›‘ AND THE TINT STOPS BEING A FLASH

Normally --ds-tint-cool under a frame is what shows between layout and decode, so a grid of frames does not flicker as it loads. A contained picture rarely fills its ratio, so that tint is what a reader sees around it for as long as the page is open.

That is a feature rather than a side effect: artwork drawn on transparency gets a surface to sit on, in whichever theme is nearest it. A caller who wants a different mat moves --ds-tint-cool on the subtree β€” or puts a data-theme on it β€” rather than painting a colour at the call site.

THE LINK COVERS THE FRAME, IT DOES NOT WRAP IT

An <a> may not contain interactive content, and a clip's pause button is exactly that β€” so wrapping the frame produces invalid markup and a control nobody can press. The anchor is a sibling of the media, stretched over the box.

It goes after the media and before any control. clip makes the frame a stacking context, so what sits on top inside it is decided by document order alone. Put the link last and it swallows the pause button β€” silently, and only for the people who go looking for it.

It paints nothing. A photograph does not need an underline, and the title beside it is usually a link to the same place; the focus ring is inset, because the frame's overflow: hidden would clip an outset one into stubs. The name is the caller's β€” a link whose content is nothing announces as nothing. A check refused that until the interaction pass was removed on 21 August 2026; nothing catches it now.

NAME IT, OR HIDE IT β€” AND THE CARD BESIDE IT DECIDES WHICH

If the picture is the only way to the destination, name the anchor. The specimen above does, because there is nothing else to press.

On a card there usually is: the title beside it links to the same place. A named overlay then announces that destination twice and puts a second stop in the tab order on the way to the next card. Mark it aria-hidden="true" tabindex="-1" and it becomes what it really is β€” a bigger target for a pointer, and nothing at all to anybody else. That is the same pair of attributes a form's honeypot carries, and for the same reason: removed from the accessibility tree and from the tab order is the only honest way to say "not a target".

A LINKED CLIP GIVES UP THE BROWSER'S OWN PLAY BUTTON

With no script a clip keeps its controls so it can still be played, and an overlay link sits on top of them. There is no arrangement where both work, so it is a choice the caller makes: media that navigates drops controls; media that plays takes no link.

In practice that decides itself. On a card in a list the reader wants the project, so the picture navigates. On the project's own page there is nothing to link to β€” so it keeps its controls, and no-script playback survives exactly where it was going to be used.

IT CROPS, AND THAT IS THE JOB

object-fit: cover fills the ratio and cuts the overflow, which is what lets a row of differently-sized sources read as one deliberate set. An image whose subject sits at the very edge will lose that subject β€” a content decision, answered with a better crop in the file rather than a rule here. The specimens above use a flat swatch and so cannot show you this happening; it is the one behaviour on this page you have to take on trust, and the reason the note says so rather than staying quiet about it.

SET THE RATIO, NOT THE HEIGHT

--ratio takes an aspect-ratio expression β€” 16/9, 4/5, 21/9 β€” and defaults to 16:9. A consuming site names its ratios once, as its own classes, rather than spelling the fraction at every call site.

πŸ›‘ AND WHEN YOU DO SET A HEIGHT, SET THE WIDTH TOO

There is one good reason to reach past the ratio: a wall where every box in a row must share one height, so each frame fills the cell it is given. The obvious spelling is block-size: 100% and it has a sharp edge.

aspect-ratio sizes whichever axis is auto. Give it a definite height and leave the width alone, and it computes the width. Measured on a consuming site: a frame filling a 227px cell with --ratio: 21/9 came out 530px wide inside a 365px column, at every viewport - 530 is 227 x 21/9, and a width that never changes with the window is the tell. It overflowed the page and swallowed its neighbour's gutter, which reads as a broken gap rather than a broken width.

So a frame that takes its height from its container states both axes: --ratio: auto, inline-size: 100%, block-size: 100%. Then the ratio cannot decide either one. Stating only the height is the version that looks right in the row you wrote it in.

A SOURCE SET NEEDS THE WRAPPER STYLED TOO

A responsive image wraps its img in picture, which is an inline element by default β€” so the image would size against a line box and the ratio would quietly stop holding. Both are addressed, which is why swapping a plain image for a source set changes nothing about the layout.

THE TINT UNDERNEATH IS NOT DECORATION

It is what shows between layout and decode. Without it the box flashes the page background, and a grid of frames appears to flicker as the page loads.