Theme
On this page (4)
Component Skill — components/gallery-item/component-skill.md

Native basis

A <figure>: a <button commandfor command="show-modal"> around the thumbnail, a <figcaption>, and a <dialog> with the large picture, its caption and a close button in a <form method="dialog">.

Web Platform APIs

<figure><dialog>commandfor / command@starting-styleobject-fit

Classes

.mk-gallery-item.mk-gallery-item-open.mk-gallery-item-zoom.mk-gallery-item-caption.mk-gallery-item-viewer.mk-gallery-item-viewer-inner.mk-gallery-item-close

Data attributes

data-variant: overlay, framed.

§Default

The picture is a button with commandfor / command="show-modal": it opens the large view in a native modal <dialog> (focus trap, Escape, backdrop - all built in). The close button sits in a method="dialog" form. Click a picture.

§Overlay

data-variant='overlay': the caption slides over the picture on hover or keyboard focus.

§Framed

data-variant='framed': a white print frame with a shadow, turned a little - it straightens on hover.

§CSS view file

/* -- Gallery Item (website · Media) ---------------------------------------
   Presents one image with a caption and enlarged-view action - the picture opens a large view in a native modal dialog. */
@layer components {
  /* every box in the block measures border-box - the same on any page */
  :where(.mk-gallery-item, .mk-gallery-item *, .mk-gallery-item ::before, .mk-gallery-item ::after) { box-sizing: border-box; }
  .mk-gallery-item { position: relative; display: grid; gap: 0.625rem; margin: 0; }
  .mk-gallery-item-open {
    position: relative;
    display: block;
    overflow: hidden;
    padding: 0;
    border: 0;
    border-radius: var(--radius-lg);
    background: var(--muted);
    cursor: zoom-in;
    & img { display: block; width: 100%; aspect-ratio: 4 / 3; object-fit: cover; transition: scale 400ms ease; }
    &:hover img { scale: 1.04; }
    &:focus-visible { outline: 2px solid var(--ring); outline-offset: 2px; }
  }
  .mk-gallery-item-zoom {
    position: absolute;
    top: 0.625rem;
    right: 0.625rem;
    display: grid;
    place-items: center;
    width: 2rem;
    height: 2rem;
    border-radius: 999px;
    background: oklch(0.12 0.02 260 / 0.55);
    color: #fff;
    backdrop-filter: blur(6px);
    opacity: 0;
    transition: opacity 200ms ease;
    & svg { width: 1rem; height: 1rem; }
  }
  .mk-gallery-item-open:is(:hover, :focus-visible) .mk-gallery-item-zoom { opacity: 1; }
  .mk-gallery-item-caption { font-size: 0.875rem; font-weight: 500;
    & span { display: block; color: var(--muted-foreground); font-weight: 400; font-size: 0.8125rem; }
  }
  /* the large view */
  .mk-gallery-item-viewer {
    max-width: min(92vw, 72rem);
    max-height: 92vh;
    padding: 0;
    border: 0;
    background: transparent;
    color: #fff;
    overflow: visible;
    opacity: 1;
    scale: 1;
    transition: opacity 250ms ease, scale 250ms ease, display 250ms allow-discrete, overlay 250ms allow-discrete;
    &::backdrop { background: oklch(0.1 0.01 260 / 0.85); backdrop-filter: blur(6px); }
    &:not([open]) { opacity: 0; scale: 0.96; }
  }
  @starting-style {
    .mk-gallery-item-viewer[open] { opacity: 0; scale: 0.96; }
  }
  .mk-gallery-item-viewer-inner {
    position: relative;
    display: grid;
    gap: 0.75rem;
    & img { display: block; max-width: 100%; max-height: 80vh; margin-inline: auto; border-radius: var(--radius-lg); object-fit: contain; }
    & p { margin: 0; text-align: center; font-size: 0.9375rem; }
    & p span { color: oklch(1 0 0 / 0.65); }
    & form { position: absolute; top: -0.75rem; right: -0.75rem; }
  }
  .mk-gallery-item-close {
    display: grid;
    place-items: center;
    width: 2.25rem;
    height: 2.25rem;
    padding: 0;
    border: 0;
    border-radius: 999px;
    background: #fff;
    color: oklch(0.15 0 0);
    box-shadow: var(--shadow-lg);
    cursor: pointer;
    & svg { width: 1.125rem; height: 1.125rem; }
    &:focus-visible { outline: 2px solid #fff; outline-offset: 2px; }
  }
  .mk-gallery-item[data-variant="overlay"] {
    & .mk-gallery-item-caption { position: absolute; inset: auto 0 0; z-index: 1; padding: 2rem 1rem 0.875rem; border-radius: 0 0 var(--radius-lg) var(--radius-lg); background: linear-gradient(transparent, oklch(0.12 0.02 260 / 0.85)); color: #fff; opacity: 0; transition: opacity 200ms ease; pointer-events: none; }
    & .mk-gallery-item-caption span { color: oklch(1 0 0 / 0.75); }
    &:is(:hover, :focus-within) .mk-gallery-item-caption { opacity: 1; }
  }
  .mk-gallery-item[data-variant="framed"] {
    padding: 0.75rem 0.75rem 1rem;
    background: #fff;
    color: oklch(0.2 0 0);
    box-shadow: var(--shadow-lg);
    rotate: -1.5deg;
    transition: rotate 300ms ease;
    &:nth-child(even) { rotate: 1.5deg; }
    &:hover { rotate: 0deg; }
    & .mk-gallery-item-open { border-radius: 2px; }
    & .mk-gallery-item-caption { font-family: var(--font-serif); text-align: center; }
    & .mk-gallery-item-caption span { color: oklch(0.45 0 0); }
  }
  /* -- Accessibility ------------------------------------------- */
  @media (prefers-reduced-motion: reduce) {
    .mk-gallery-item-open img, .mk-gallery-item-zoom, .mk-gallery-item-viewer, .mk-gallery-item[data-variant="framed"], .mk-gallery-item[data-variant="overlay"] .mk-gallery-item-caption { transition: none; }
    .mk-gallery-item-open:hover img { scale: none; }
  }
  @media (forced-colors: active) {
    .mk-gallery-item-close { border: 1px solid ButtonText; }
  }
}

Comments, ideas or improvements? Edit this page's source on GitHub