Visual

An image or video slot that keeps a fixed aspect ratio — nine ratios, optional rounded corners, a darkening hover overlay with a fullscreen action, and a centred play button for video.

Built withButton

Examples

Aspect ratios

The nine ratios. The visual fills its container’s width and derives its height from the ratio, so it slots into any grid or track.

<div style="display:flex;flex-wrap:wrap;align-items:flex-start;gap:1rem">
<div style="width:10rem"><figure class="kompan-visual kompan-visual_21x9"><img src="https://cdn.sanity.io/images/aqjgxzv4/production/49d040d99874d92e901541340b7cf4f7ca7a6533-1600x1067.jpg" alt=""></figure><p style="margin:0.375rem 0 0;font:12px system-ui;color:#666">21:9 · Widest</p></div>
<div style="width:10rem"><figure class="kompan-visual kompan-visual_16x9"><img src="https://cdn.sanity.io/images/aqjgxzv4/production/49d040d99874d92e901541340b7cf4f7ca7a6533-1600x1067.jpg" alt=""></figure><p style="margin:0.375rem 0 0;font:12px system-ui;color:#666">16:9 · Wider</p></div>
<div style="width:10rem"><figure class="kompan-visual kompan-visual_3x2"><img src="https://cdn.sanity.io/images/aqjgxzv4/production/49d040d99874d92e901541340b7cf4f7ca7a6533-1600x1067.jpg" alt=""></figure><p style="margin:0.375rem 0 0;font:12px system-ui;color:#666">3:2 · Wide</p></div>
<div style="width:10rem"><figure class="kompan-visual kompan-visual_6x5"><img src="https://cdn.sanity.io/images/aqjgxzv4/production/49d040d99874d92e901541340b7cf4f7ca7a6533-1600x1067.jpg" alt=""></figure><p style="margin:0.375rem 0 0;font:12px system-ui;color:#666">6:5 · Square'ish</p></div>
<div style="width:10rem"><figure class="kompan-visual kompan-visual_7x5"><img src="https://cdn.sanity.io/images/aqjgxzv4/production/49d040d99874d92e901541340b7cf4f7ca7a6533-1600x1067.jpg" alt=""></figure><p style="margin:0.375rem 0 0;font:12px system-ui;color:#666">7:5 · Square'ish</p></div>
<div style="width:10rem"><figure class="kompan-visual kompan-visual_5x4"><img src="https://cdn.sanity.io/images/aqjgxzv4/production/49d040d99874d92e901541340b7cf4f7ca7a6533-1600x1067.jpg" alt=""></figure><p style="margin:0.375rem 0 0;font:12px system-ui;color:#666">5:4 · Square'ish</p></div>
<div style="width:10rem"><figure class="kompan-visual kompan-visual_1x1"><img src="https://cdn.sanity.io/images/aqjgxzv4/production/bb4a81fd245831de26b47bc1a62a43afd75ef3b9-1600x1066.jpg" alt=""></figure><p style="margin:0.375rem 0 0;font:12px system-ui;color:#666">1:1 · Square</p></div>
<div style="width:10rem"><figure class="kompan-visual kompan-visual_4x5"><img src="https://cdn.sanity.io/images/aqjgxzv4/production/ab9654ba02d9d75a5e993f6bda3fa882bd783bb0-1000x667.jpg" alt=""></figure><p style="margin:0.375rem 0 0;font:12px system-ui;color:#666">4:5 · High</p></div>
<div style="width:10rem"><figure class="kompan-visual kompan-visual_2x3"><img src="https://cdn.sanity.io/images/aqjgxzv4/production/ab9654ba02d9d75a5e993f6bda3fa882bd783bb0-1000x667.jpg" alt=""></figure><p style="margin:0.375rem 0 0;font:12px system-ui;color:#666">2:3 · Highest</p></div>
</div>

Rounded

Corner radius=True in Figma — the image radius token.

<div style="display:flex;flex-wrap:wrap;align-items:flex-start;gap:1rem">
<div style="width:16rem"><figure class="kompan-visual kompan-visual_3x2 kompan-visual_rounded"><img src="https://cdn.sanity.io/images/aqjgxzv4/production/bb4a81fd245831de26b47bc1a62a43afd75ef3b9-1600x1066.jpg" alt="Children climbing the owl tower"></figure></div>
<div style="width:12rem"><figure class="kompan-visual kompan-visual_4x5 kompan-visual_rounded"><img src="https://cdn.sanity.io/images/aqjgxzv4/production/ab9654ba02d9d75a5e993f6bda3fa882bd783bb0-1000x667.jpg" alt="A girl looking up through a climbing tube"></figure></div>
</div>

Video

A centred play button is always visible when the media is a video. The button is the Button atom at 84px.

<div style="width:24rem">
<figure class="kompan-visual kompan-visual_16x9 kompan-visual_video">
  <img src="https://cdn.sanity.io/images/aqjgxzv4/production/25daa91b33fe981b44036954c2670e6e82edc727-1600x1066.jpg" alt="Children queuing for a wide slide">
  <div class="kompan-visual__overlay">
    <button type="button" class="kompan-button kompan-button_lg kompan-button_icon-only kompan-visual__play" aria-label="Play video"><svg class="kompan-button__icon" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M8 5.5v13a1 1 0 0 0 1.5.87l11-6.5a1 1 0 0 0 0-1.74l-11-6.5A1 1 0 0 0 8 5.5Z"/></svg></button>
  </div>
</figure>
</div>

Gallery (hover)

When the media opens a full-screen gallery, hovering darkens the image and reveals the fullscreen action in the top-left corner. Keyboard users get the same via focus.

<div style="width:24rem">
<figure class="kompan-visual kompan-visual_3x2 kompan-visual_hover">
  <img src="https://cdn.sanity.io/images/aqjgxzv4/production/9a969aae62a5185f4db5ff973cf7eedc69619cc9-1000x667.jpg" alt="Two girls peeking through a wooden climbing wall">
  <div class="kompan-visual__overlay">
    <span class="kompan-visual__action">
      <button type="button" class="kompan-button kompan-button_secondary-dark kompan-button_xs kompan-button_icon-only" aria-label="Open in fullscreen"><svg class="kompan-button__icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M15 3h6v6M9 21H3v-6M21 3l-7 7M3 21l7-7"/></svg></button>
    </span>
  </div>
</figure>
</div>

Accessibility

Every image needs alt text

Describe what the picture shows in alt (what a sighted visitor gets from it), or use alt="" when it is purely decorative. A caption does not replace alt text.

Hover is not the only way in

The overlay also appears on :focus-within, so the fullscreen and play buttons are reachable by keyboard. Keep them real buttons with aria-label, and give the fullscreen gallery a way to close.

Video

The play button starts playback; once playing, hand over to the native or custom video controls (play/pause, captions, volume). Do not autoplay with sound.

Contrast on top of photos

The overlay darkens the image by 5% only, so it does not guarantee contrast for text. Put text on solid surfaces, not on the visual.

Stylesheet

Shipped as @wah-kompan/kompan-ui/components/visual.css. Every value is a token reference, so it themes with the rest of the system rather than being restyled. The shipped file also imports Button — what this component is built with — so one import is enough.

css
.kompan-visual {
  --kompan-visual-ratio: 3 / 2;
  --kompan-visual-radius: 0;
  --kompan-visual-overlay: var(--color-utility-alpha-black-5);

  position: relative;
  display: block;
  box-sizing: border-box;
  margin: 0;
  overflow: hidden;
  aspect-ratio: var(--kompan-visual-ratio);
  border-radius: var(--kompan-visual-radius);
  background: var(--color-bg-secondary);
}

.kompan-visual > img,
.kompan-visual > video,
.kompan-visual > picture,
.kompan-visual > picture > img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* Aspect ratios — Figma's nine "Aspect" variants */
.kompan-visual_21x9 { --kompan-visual-ratio: 21 / 9; }
.kompan-visual_16x9 { --kompan-visual-ratio: 16 / 9; }
.kompan-visual_3x2 { --kompan-visual-ratio: 3 / 2; }
.kompan-visual_6x5 { --kompan-visual-ratio: 6 / 5; }
.kompan-visual_7x5 { --kompan-visual-ratio: 7 / 5; }
.kompan-visual_5x4 { --kompan-visual-ratio: 5 / 4; }
.kompan-visual_1x1 { --kompan-visual-ratio: 1 / 1; }
.kompan-visual_4x5 { --kompan-visual-ratio: 4 / 5; }
.kompan-visual_2x3 { --kompan-visual-ratio: 2 / 3; }

.kompan-visual_rounded {
  --kompan-visual-radius: var(--radius-image-m);
}

/* Overlay — darkens on hover (or always, for video) and hosts the actions */
.kompan-visual__overlay {
  position: absolute;
  inset: 0;
  display: grid;
  place-items: center;
  background: var(--kompan-visual-overlay);
  opacity: 0;
  transition: opacity 0.2s ease;
}

.kompan-visual_hover:hover > .kompan-visual__overlay,
.kompan-visual:focus-within > .kompan-visual__overlay,
.kompan-visual_video > .kompan-visual__overlay {
  opacity: 1;
}

/* Gallery: a small utility button in the top-left corner */
.kompan-visual__action {
  position: absolute;
  top: var(--spacing-xl);
  left: var(--spacing-xl);
}

/* Video: the centred play button is a large primary icon-only Button (84px) */
.kompan-visual__play {
  --kompan-button-py: var(--spacing-3xl);
  --kompan-button-px: var(--spacing-3xl);
  --kompan-button-icon: 2.25rem;
  border-radius: var(--radius-full);
}

@media (prefers-reduced-motion: reduce) {
  .kompan-visual__overlay {
    transition: none;
  }
}