Carousel with Scroll Controls

anchor-positioning
scroll-snap
Preview

Native carousel using scroll-snap with anchor-positioned controls and scroll markers.

Set anchor-name for button positioning and scroll-marker-group: after for marker placement.

<ul class="carousel">
  <li>
    <figure>
      <img src="/images/jeremy-hynes-zLrqHNms8eE-unsplash.jpg" alt="">
      <figcaption>
        Photo by <a target="_parent" href="https://unsplash.com/@hynesight?utm_content=creditCopyText&utm_medium=referral&utm_source=unsplash">Jeremy Hynes</a> on <a target="_parent" href="https://unsplash.com/photos/brown-owl-on-tree-branch-covered-with-snow-zLrqHNms8eE?utm_content=creditCopyText&utm_medium=referral&utm_source=unsplash">Unsplash</a>
      </figcaption>
    </figure>
  </li>
  <li>
    <figure>
      <img src="/images/richard-jacobs-8oenpCXktqQ-unsplash.jpg" alt="">
      <figcaption>
        Photo by <a target="_parent" href="https://unsplash.com/@rj2747?utm_content=creditCopyText&utm_medium=referral&utm_source=unsplash">Richard Jacobs</a> on <a target="_parent" href="https://unsplash.com/photos/grayscale-photo-of-elephants-drinking-water-8oenpCXktqQ?utm_content=creditCopyText&utm_medium=referral&utm_source=unsplash">Unsplash</a>
      </figcaption>
    </figure>
  </li>
  <li>
    <figure>
      <img src="/images/rod-long-gUYYvPrnuHY-unsplash.jpg" alt="">
      <figcaption>
        Photo by <a target="_parent" href="https://unsplash.com/@rodlong?utm_content=creditCopyText&utm_medium=referral&utm_source=unsplash">Rod Long</a> on <a target="_parent" href="https://unsplash.com/photos/black-whale-in-water-during-daytime-gUYYvPrnuHY?utm_content=creditCopyText&utm_medium=referral&utm_source=unsplash">Unsplash</a>
      </figcaption>
    </figure>
  </li>
</ul>
  .carousel {
    container: inline-size;
    display: flex;
    gap: 20px;
    overflow-x: scroll;
    scroll-snap-type: x mandatory;
    scroll-behavior: smooth;
    list-style: none;
    position: relative;
    anchor-name: --carousel;
    scroll-marker-group: after;
    scrollbar-width: none;
  }

  .carousel li {
    flex: 0 0 100%;
    scroll-snap-align: center;
    container-type: scroll-state;

    @supports (container-type: scroll-state) {
      @media (prefers-reduced-motion: no-preference) {
        figcaption {
          transition: transform 0.3s ease-in-out;
          transform: translateY(100%);

          @container scroll-state(snapped: x) {
            transform: translateY(0);
          }
        }
      }
    }
  }

  figure {
    aspect-ratio: 16 / 9;
    display: grid;
    place-items: center;
    font-size: 3rem;
    font-weight: bold;
    border-radius: 8px;
    overflow: hidden;
    position: relative;
  }

  figure figcaption {
    position: absolute;
    bottom: 0;
    width: 100%;
    background-image: linear-gradient(to top, rgba(0, 0, 0, 0.7), rgba(0, 0, 0, 0));
    color: white;
    padding: 40px 10px 10px;
    font-size: 0.8rem;
    text-align: left;
    font-weight: lighter;
  }

  figcaption a {
    color: white;
  }

Scroll Button Controls

::scroll-button pseudo-elements anchored to carousel using position-anchor with directional positioning.

  .carousel::scroll-button(*) {
    font-family: "Material Icons";
    position: absolute;
    position-anchor: --carousel;
    /* make them round and easy to press */
    inline-size: 48px;
    aspect-ratio: 1;
    border-radius: 9999px;
    background-color: #fff;
    font-size: 20px;
    border: 1px solid #999;
    transition: all 0.1s ease-in-out;
  }

  .carousel::scroll-button(*):focus-visible {
    outline-offset: 4px;
  }

  .carousel::scroll-button(*):disabled {
    opacity: 0.3;
  }

  .carousel::scroll-button(*):not(:disabled):is(:hover, :active) {
    background-color: rgba(255, 255, 255, 0.8);
  }

  .carousel::scroll-button(*):not(:disabled):active {
    scale: 95%;
    transform-origin: center;
  }

  .carousel::scroll-button(left) {
    content: 'chevron_left' / 'Previous';
    position-area: center;
    translate: calc(var(--carousel-item-half-width) * -1 + 36px);
  }

  .carousel::scroll-button(right) {
    content: 'chevron_right' / 'Next';
    position-area: center;
    translate: calc(var(--carousel-item-half-width) - 36px);
  }

Marker Group Layout

::scroll-marker-group creates auto-flow grid container with scroll handling for marker overflow.

  .carousel::scroll-marker-group {
    grid-area: markers; /* place markers in parent grid area */

    /* 15px by 15px horizontal grid - size of dots */
    display: grid;
    place-content: safe center;
    grid: 12px / auto-flow 12px;
    gap: 20px;
    padding: 15px;
    scroll-padding: 15px;

    /* handle overflow */
    overflow: auto;
    overscroll-behavior-x: contain;
    scrollbar-width: none;
    scroll-snap-type: x mandatory;

    @media (prefers-reduced-motion: no-preference) {
      scroll-behavior: smooth;
    }
  }

Individual Markers

::scroll-marker styles each indicator with :target-current highlighting the active slide.

  .carousel li::scroll-marker {
    content: ''/ attr(data-name);
    width: 10px;
    height: 10px;
    border-radius: 9999px;
    background-color: white;
    border: 1px solid black;
    /* snap if group is overflowing */
    scroll-snap-align: center;
  }

  .carousel li::scroll-marker:is(:hover, :focus-visible) {
    border-color: gray;
  }

  .carousel li::scroll-marker:target-current {
    background: black;
    border-color: black;
  }