Theme
Design your own
On this page (2)
Component Skill — components/media-gallery/component-skill.md

Native basis

A <section>: a header with the count, a radio group for the layout (grid / masonry / filmstrip - :has(:checked) switches the list), a slideshow Button (commandfor → <dialog>), the pictures as a list of <figure>s, and the dialog with a scroll-snap track.

Built from

Button, Dialog.

Web Platform APIs

:has()CSS scroll snap<dialog>commandfor / commandobject-fit

Classes

.mk-media-gallery.mk-media-gallery-head.mk-media-gallery-tools.mk-media-gallery-views.mk-media-gallery-grid.mk-media-gallery-viewer.mk-media-gallery-viewer-inner.mk-media-gallery-track.mk-media-gallery-close.mk-media-gallery-hint

Data attributes

The layout radios (value="grid|masonry|strip").

§Default

Three radios switch the layout - grid, masonry (CSS columns, every picture in its own ratio) and filmstrip (snapping row) - read by :has(), no script. "Slideshow" opens a modal <dialog> whose track scroll-snaps per picture; swipe, scroll or use the arrow keys.

§CSS view file

/* -- Media Gallery (website · Media) --------------------------------------
   Coordinates a collection of images with shared viewing controls - switch between grid, masonry and filmstrip, and open a slideshow of every picture. */
@layer components {
  /* every box in the block measures border-box - the same on any page */
  :where(.mk-media-gallery, .mk-media-gallery *, .mk-media-gallery ::before, .mk-media-gallery ::after) {
    box-sizing: border-box;
  }
  .mk-media-gallery {
    container: mk-media-gallery / inline-size;
    display: grid;
    gap: 1.25rem;
    width: 100%;
  }
  .mk-media-gallery-head {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 0.75rem 1rem;
    & h2 {
      margin: 0;
      font-size: 1.25rem;
      font-weight: 600;
    }
    & > span {
      color: var(--muted-foreground);
      font-size: 0.875rem;
    }
  }
  .mk-media-gallery-tools {
    display: flex;
    gap: 0.5rem;
    margin-inline-start: auto;
  }
  .mk-media-gallery-views {
    display: inline-flex;
    padding: 0.1875rem;
    border-radius: var(--radius-md);
    background: var(--muted);
    & label {
      position: relative;
      display: grid;
      place-items: center;
      width: 2rem;
      height: 1.75rem;
      border-radius: calc(var(--radius-md) - 2px);
      color: var(--muted-foreground);
      cursor: pointer;
    }
    & input {
      position: absolute;
      inset: 0;
      margin: 0;
      opacity: 0;
      cursor: pointer;
    }
    & svg {
      width: 1rem;
      height: 1rem;
    }
    & label:has(:checked) {
      background: var(--background);
      color: var(--foreground);
      box-shadow: var(--shadow-xs);
    }
    & label:has(:focus-visible) {
      outline: 2px solid var(--ring);
      outline-offset: 1px;
    }
  }
  .mk-media-gallery-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(min(100%, 11rem), 1fr));
    gap: 0.625rem;
    margin: 0;
    padding: 0;
    list-style: none;
    & figure {
      margin: 0;
      overflow: hidden;
      border-radius: var(--radius-md);
      background: var(--muted);
    }
    & img {
      display: block;
      width: 100%;
      aspect-ratio: 1;
      object-fit: cover;
    }
  }
  .mk-media-gallery:has(input[value="masonry"]:checked) .mk-media-gallery-grid {
    display: block;
    columns: 11rem;
    column-gap: 0.625rem;
    & li {
      margin-block-end: 0.625rem;
      break-inside: avoid;
    }
    & img {
      aspect-ratio: auto;
      height: auto;
    }
  }
  .mk-media-gallery:has(input[value="strip"]:checked) .mk-media-gallery-grid {
    display: flex;
    overflow-x: auto;
    overscroll-behavior-x: contain;
    scroll-snap-type: x mandatory;
    padding-block-end: 0.5rem;
    & li {
      flex: 0 0 min(70%, 18rem);
      scroll-snap-align: start;
    }
    & img {
      aspect-ratio: 4 / 3;
    }
  }
  /* the slideshow */
  .mk-media-gallery-viewer {
    width: min(94vw, 64rem);
    max-width: none;
    padding: 0;
    border: 0;
    background: transparent;
    color: #fff;
    overflow: visible;
    &::backdrop {
      background: oklch(0.08 0.01 260 / 0.92);
    }
  }
  .mk-media-gallery-viewer-inner {
    position: relative;
    display: grid;
    gap: 1rem;
    & form {
      position: absolute;
      top: -3rem;
      right: 0;
    }
  }
  .mk-media-gallery-track {
    display: flex;
    margin: 0;
    padding: 0;
    overflow-x: auto;
    overscroll-behavior-x: contain;
    scroll-snap-type: x mandatory;
    scrollbar-width: none;
    list-style: none;
    border-radius: var(--radius-lg);
    & li {
      flex: 0 0 100%;
      scroll-snap-align: center;
      display: grid;
      gap: 0.75rem;
    }
    & img {
      display: block;
      width: 100%;
      max-height: 75vh;
      object-fit: contain;
    }
    & p {
      margin: 0;
      text-align: center;
      color: oklch(1 0 0 / 0.75);
      font-size: 0.875rem;
    }
    &:focus-visible {
      outline: 2px solid #fff;
      outline-offset: 4px;
    }
  }
  .mk-media-gallery-close {
    display: grid;
    place-items: center;
    width: 2.25rem;
    height: 2.25rem;
    padding: 0;
    border: 0;
    border-radius: 999px;
    background: oklch(1 0 0 / 0.15);
    color: #fff;
    cursor: pointer;
    & svg {
      width: 1.125rem;
      height: 1.125rem;
    }
    &:hover {
      background: oklch(1 0 0 / 0.25);
    }
    &:focus-visible {
      outline: 2px solid #fff;
      outline-offset: 2px;
    }
  }
  .mk-media-gallery-hint {
    margin: 0;
    text-align: center;
    color: oklch(1 0 0 / 0.55);
    font-size: 0.75rem;
  }
  /* -- Accessibility ------------------------------------------- */
  @media (prefers-reduced-motion: reduce) {
    .mk-media-gallery-track,
    .mk-media-gallery-grid {
      scroll-behavior: auto;
    }
  }
  @media (forced-colors: active) {
    .mk-media-gallery-views label:has(:checked) {
      outline: 2px solid Highlight;
    }
  }
}

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