body
  section.section
    .grid
      .section-img
        img(src="https://picsum.photos/400/400?random=1", alt="")
      .section-text
        |Lorem ipsum dolor sit amet consectetur adipisicing elit. Ratione molestias itaque ducimus quasi. At nam quisquam atque neque autem, facere architecto. Explicabo porro tempora corporis quos obcaecati perferendis doloribus perspiciatis.
  .marquee(data-marquee-speed="60" data-marquee-direction="right")
    .marquee-wrapper
      .marquee-content
        - for (var i = 0; i < 5; i++)
          .marquee-item
            .content-img
              img(src=`https://picsum.photos/id/${i+100}/600/400`, alt="")

  section.section
    .grid
      .section-text
        |Lorem ipsum dolor sit amet consectetur adipisicing elit. Ratione molestias itaque ducimus quasi. At nam quisquam atque neque autem, facere architecto. Explicabo porro tempora corporis quos obcaecati perferendis doloribus perspiciatis.
      .section-img
        img(src="https://picsum.photos/400/400?random=2", alt="")
  .marquee(data-marquee-speed="60")
    .marquee-wrapper
      .marquee-content
        .marquee-item
          .content-text
            |SAMPLE TEXT

  section.section
    .grid
      .section-img
        img(src="https://picsum.photos/400/400?random=1", alt="")
      .section-text
        |Lorem ipsum dolor sit amet consectetur adipisicing elit. Ratione molestias itaque ducimus quasi. At nam quisquam atque neque autem, facere architecto. Explicabo porro tempora corporis quos obcaecati perferendis doloribus perspiciatis.
  .marquee(data-marquee-speed="60" data-marquee-hover="true")
    .marquee-wrapper
      .marquee-content
        - for (var i = 0; i < 5; i++)
          .marquee-item
            .content-img
              img(src=`https://picsum.photos/id/${i+200}/600/400`, alt="")

  section.section
    .grid
      .section-text
        |Lorem ipsum dolor sit amet consectetur adipisicing elit. Ratione molestias itaque ducimus quasi. At nam quisquam atque neque autem, facere architecto. Explicabo porro tempora corporis quos obcaecati perferendis doloribus perspiciatis.
      .section-img
        img(src="https://picsum.photos/400/400?random=2", alt="")
  .marquee(data-marquee-speed="60" data-marquee-direction="right" data-marquee-hover="true")
    .marquee-wrapper
      .marquee-content
        .marquee-item
          .content-text
            |SAMPLE TEXT
View Compiled
* {
  margin: 0;
  padding: 0;
}

body {
  background: #333;
  color: #eee;
  padding-bottom: 50vh;
}

img {
  width: 100%;
  height: auto;
}

.section {
  height: 60vh;
  display: flex;
  justify-content: center;
  align-items: center;
  max-width: 1200px;
  margin-inline: auto;
  padding-inline: 40px;
  margin-block: 120px;
}

.grid {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: 40px;
  align-items: center;
}

.marquee {
  overflow-x: hidden;
}

.marquee-wrapper {
  display: flex;
  gap: 24px;
}

.marquee-content {
  display: flex;
  gap: 24px;
}

.content-img {
  width: 320px;
  flex-shrink: 0;
}

.content-text {
  font-weight: 700;
  font-size: 60px;
  white-space: nowrap;
}
View Compiled
class Marquee {
  constructor() {
    this.els = document.querySelectorAll('.marquee');
    console.log(this.els);
    if (!this.els) return;

    this.init()
  }
  init() {
    this.els.forEach(el => {
      // 要素が非表示の場合は処理をスキップ
      if (getComputedStyle(el).display === 'none') return;

      const options = {
        speed: el.dataset.marqueeSpeed || 60,
        direction: el.dataset.marqueeDirection || 'left',
        pauseOnHover: el.dataset.marqueeHover === 'true'
      };
      this.Marquee(el, options);
    })
  }
  Marquee(el, options) {
    const { speed, direction, pauseOnHover } = options;
    const wrapper = el.querySelector('.marquee-wrapper');
    const content = el.querySelector('.marquee-content');
    this.appendContent(content, wrapper);
    this.updateWrapperWidth(content, wrapper);
    const animation = this.Animation(wrapper, speed, direction);

    this.hoverEvent(el, animation, pauseOnHover);
    this.observerEvent(el, animation);
  }
  updateWrapperWidth(content, wrap) {
    const contentWidth = content.getBoundingClientRect().width;
    console.log(contentWidth);
    if (contentWidth === 0) return;
    const gap = parseInt(getComputedStyle(content).columnGap);
    wrap.style.width = `${contentWidth + gap}px`;
  }
  appendClone(content, wrap) {
    const clone = content.cloneNode(true);
    wrap.appendChild(clone);
  }
  appendContent(content, wrap) {
    const innerWidth = window.innerWidth;
    const contentWidth = content.getBoundingClientRect().width;

    this.appendClone(content, wrap);

    if (contentWidth < innerWidth) {
      const numClones = Math.ceil(innerWidth / contentWidth);
      for (let i = 0; i < numClones; i++) {
        this.appendClone(content, wrap);
      }
    }
  }
  Animation(wrap, speed, direction) {
    const wrapWidth = wrap.getBoundingClientRect().width;

    if (wrapWidth === 0) return;
    const keyframes = direction === 'left' ?
      [{ translate: '0 0' }, { translate: '-100% 0' }] :
      [{ translate: '-100% 0' }, { translate: '0 0' }];
    const options = {
      duration: (wrapWidth / speed) * 1000,
      iterations: Infinity,
    }
    return wrap.animate(keyframes, options);
  }
  hoverEvent(el, animation, hasOpt) {
    if (!hasOpt) return;
    el.addEventListener('mouseenter', () => animation.pause());
    el.addEventListener('mouseleave', () => animation.play());
  }
  observerEvent(el, animation) {
    const observerOptions = {
      root: null,
      threshold: 0,
    };

    const observer = new IntersectionObserver((entry) => {
      entry[0].isIntersecting ? animation.play() : animation.pause();
    }, observerOptions);

    observer.observe(el);
  }
}

const marquee = new Marquee();

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

This Pen doesn't use any external JavaScript resources.