Theme
Design your own
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