<scroll-snap-carousel alignment="center">
  <iframe src="https://www.youtube.com/embed/3ZTvsUeQkOM?si=iNSnBcZRKWsMUB2e" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>
  <iframe src="https://www.youtube.com/embed/7Dr5LW9xnSs?si=aLyux8R2QGk61XwU" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>
  <iframe src="https://www.youtube.com/embed/31lbp1dolAI?si=B1SYEzLmlZ8QN3pG" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>
  <iframe src="https://www.youtube.com/embed/onthvMAIpUI?si=XZK9y1OkfAhMJUFY" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>
  <iframe src="https://www.youtube.com/embed/Ij-1kXYKD3c?si=ha_b0VuYevv-6q-V" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>
  <iframe src="https://www.youtube.com/embed/RFdLLDmTTk8?si=1RVxxa3Hw-nL8ph8" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>

  <p slot="disclaimer">
    Check out <a href="https://rayray.komi.io/" target="_blank" rel="noopener noreferrer">RayRay!</a>
  </p>
</scroll-snap-carousel>
:root {
  align-items: center;
  background: linear-gradient(45deg, #82ff2e, #ff7c7c);
  display: flex;
  height: 100%;
  justify-content: center;
}

iframe {
  aspect-ratio: 16/9;
  border-radius: 10px;
  width: 75%;
  max-height: 700px;
}

p {
  font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas,
    "Liberation Mono", "Courier New", monospace;
  font-size: 14px;
  padding-top: 8px;
}

scroll-snap-container {
  margin: 16px;
}
/**
 * A custom element that creates a scrollable carousel with snap points.
 * Based on an example from this article: https://web.dev/articles/css-scroll-snap
 *
 * @attr {'start' | 'center' | 'end' | 'none'} alignment - The alignment of the carousel scroll snap.
 */
class ScrollSnapCarousel extends HTMLElement {
  /**
   * Define the properties of the custom element.
   */
  static get observedAttributes() {
    return ["alignment"];
  }

  /**
   * Create a new instance of the custom element.
   */
  constructor() {
    super();
    this.shadow = this.attachShadow({ mode: "open" });
    this.alignment = "start";

    this.scrollToNextPage = this.scrollToNextPage.bind(this);
    this.scrollToPreviousPage = this.scrollToPreviousPage.bind(this);
    this.calculateGalleryItemSize = this.calculateGalleryItemSize.bind(this);
  }

  /**
   * Called when an attribute on the custom element is changed.
   */
  attributeChangedCallback(name, oldValue, newValue) {
    if (name === "alignment") {
      this.alignment = newValue;
    }
  }

  /**
   * Called when the element is added to the DOM.
   */
  connectedCallback() {
    this.render();

    this.gallery = this.shadowRoot.querySelector("#paginated-gallery");
    this.galleryScroller = this.gallery.querySelector(".gallery-scroller");

    this.calculateGalleryItemSize();

    this.gallery
      .querySelector("button.next")
      .addEventListener("click", this.scrollToNextPage);

    this.gallery
      .querySelector("button.previous")
      .addEventListener("click", this.scrollToPreviousPage);

    /** Recalculate the gallery item size when the window is resized. */
    window.addEventListener("resize", this.calculateGalleryItemSize);
  }

  /**
   * Called when the element is removed from the DOM.
   */
  disconnectedCallback() {
    window.removeEventListener("resize", this.calculateGalleryItemSize);
  }

  /**
   * Calculate the size of the gallery items.
   */
  calculateGalleryItemSize() {
    const slotElement = this.galleryScroller.querySelector("slot");
    const nodes = slotElement.assignedNodes({ flatten: true });
    const firstSlottedElement = nodes.find(
      (node) => node.nodeType === Node.ELEMENT_NODE
    );
    this.galleryItemSize = firstSlottedElement.clientWidth;
  }

  /**
   * Scroll the carousel to the previous page.
   */
  scrollToPreviousPage() {
    this.galleryScroller.scrollBy(-this.galleryItemSize, 0);
  }

  /**
   * Scroll the carousel to the next page.
   */
  scrollToNextPage() {
    this.galleryScroller.scrollBy(this.galleryItemSize, 0);
  }

  /**
   * Handles the rendering of the component markup and CSS.
   */
  render() {
    this.shadowRoot.innerHTML = `
      <style>
        * {
          scroll-behavior: smooth;
        }
        
        ::slotted(*) {
          margin: 0 16px;
          min-width: 75%;
          position: relative;
          scroll-snap-align: ${this.alignment};
        }
        
        .gallery {
          position: relative;
        }
      
        .gallery-scroller {
          -webkit-overflow-scrolling: touch;
          align-items: center;
          display: flex;
          overflow-x: scroll;
          overflow-y: hidden;
          scroll-snap-type: x mandatory;
        }
      
        button {
          background-color: rgba(0,0,0,0.5);
          background-position: 50% 50%;
          background-repeat: no-repeat;
          border-radius: 4px;
          height: 30px;
          position: absolute;
          top: 50%;
          transform: translateY(-50%);
          width: 30px;
          z-index: 1;
        }
        
        button:hover {
          cursor: pointer;
        }
      
        button.next {
          background-image: url('data:image/svg+xml;charset=utf-8,<svg width="18" height="18" viewBox="0 0 34 34" xmlns="http://www.w3.org/2000/svg"><title>Shape</title><path d="M25.557 14.7L13.818 2.961 16.8 0l16.8 16.8-16.8 16.8-2.961-2.961L25.557 18.9H0v-4.2z" fill="%23FFF" fill-rule="evenodd"/></svg>');
          right: 10px;
        }
        
        button.previous {
          background-image: url('data:image/svg+xml;charset=utf-8,<svg width="18" height="18" viewBox="0 0 34 34" xmlns="http://www.w3.org/2000/svg"><title>Shape</title><path d="M33.6 14.7H8.043L19.782 2.961 16.8 0 0 16.8l16.8 16.8 2.961-2.961L8.043 18.9H33.6z" fill="%23FFF" fill-rule="evenodd"/></svg>');
          left: 10px;
        }
      </style>
    
      <div id="paginated-gallery" class="gallery">
          <button class="previous" aria-label="Previous"></button>
          <button class="next" aria-label="Next" ></button>
          <div class="gallery-scroller">
            <slot></slot>
          </div>
      </div>
      <slot name="disclaimer"></slot>
        `;
  }
}

window.customElements.define("scroll-snap-carousel", ScrollSnapCarousel);

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

This Pen doesn't use any external JavaScript resources.