HTML / CSS

テキストの無限ループアニメーション

CSSのみ

・ループするテキストは複数用意すること

・短い単語の場合は、テキストを横並びにしたときに表示したいエリアの横幅(画面の横幅)を超えるように複数設置する必要があります

ABCDEFG HIJKLMN 

ABCDEFG HIJKLMN 

ABCDEFG HIJKLMN 

HTML
                  
            

ABCDEFG HIJKLMN 

ABCDEFG HIJKLMN 

ABCDEFG HIJKLMN 

CSS
                  
.loop-text.-css {
  overflow: hidden; /* はみ出したテキストは表示しない */
  display: flex;
  width: 100vw;
  margin-inline: calc(50% - 50vw);
}

.loop-text.-css .loop-text__item {
  flex-shrink: 0; /* 要素を縮めたくない */
  white-space: nowrap;  /* 要素を改行させない */
  font-size: 120px;
  font-style: italic;
  font-weight: bold;
  color: #555555;
  letter-spacing: 0.05em;

  &:nth-child(odd) {
    animation: MoveLeft 24s -12s infinite linear;
    /* 24秒かけて-12秒後に無限ループさせる */
    animation-fill-mode: both;
  }

  &:nth-child(even) {
    animation: MoveLeft2 24s infinite linear;
    /* 24秒かけて無限ループさせる */
    animation-fill-mode: both;
  }
}
                  
                

・流れるアニメーションの動きはkeyframesを使う

keyframes
                  
/* keyframes */
@keyframes MoveLeft {
  from {
    transform: translateX(100%);
  }

  to {
    transform: translateX(-100%);
  }
}

@keyframes MoveLeft2 {
  from {
    transform: translateX(0);
  }

  to {
    transform: translateX(-200%);
  }
}
                  
                

JSで調整

・画面サイズや文字の長さによってアニメーションの早さが変わってしまうことを防ぐ

ABCDEFG HIJKLMN 

HTML
                  
            

ABCDEFG HIJKLMN 

CSS
                  
.loop-text.-js {
  overflow: hidden;  /* はみ出したテキストは表示しない */
  display: flex;
  width: 100vw;
  margin-inline: calc(50% - 50vw);
}

.loop-text.-js .loop-text__item {
  flex-shrink: 0;  /* 要素を縮めたくない */
  white-space: nowrap;  /* 要素を改行させない */
  font-size: 60px;
  font-style: italic;
  font-weight: bold;
  color: #555555;
  letter-spacing: 0.05em;

  &:nth-child(odd) {
    animation: MoveLeft var(--tick-duration, 24s) var(--tick-delay, -12s) infinite linear;
    animation-fill-mode: both;
  }

  &:nth-child(even) {
    animation: MoveLeft2 var(--tick-duration, 24s) infinite linear;
    animation-fill-mode: both;
  }
}

@media screen and (min-width: 769px) {
  .loop-text.-js .loop-text__item {
    font-size: 120px;
  }
}
                  
                

・流れるアニメーションの動きはkeyframesを使う

keyframes
                  
/* keyframes */
@keyframes MoveLeft {
  from {
    transform: translateX(100%);
  }

  to {
    transform: translateX(-100%);
  }
}

@keyframes MoveLeft2 {
  from {
    transform: translateX(0);
  }

  to {
    transform: translateX(-200%);
  }
}
                  
                

・流れるアニメーションの動きはkeyframesを使う

JavaScript
                  
class Main {
  constructor() {
    this.init();
  }

  init() {
    this.copyText();
    this.calculateLoopAnimationSpeed();
    this.resizeRefresh();
  }

  //リサイズ時にアニメーションの速度を再計算
  resizeRefresh() {
    const target = document.body;
    const resizeObserver = new ResizeObserver((entries) => {
      entries.forEach((entry) => {
        this.calculateLoopAnimationSpeed();
      });
    });
    resizeObserver.observe(target);
  }

  //アニメーションの速度を計算してCSS変数に
  calculateLoopAnimationSpeed() {
    const targets = document.querySelectorAll('.js-tick');
    if (!targets.length) {
      return;
    }

    const distance = window.innerWidth;
    const mql = window.matchMedia('(min-width: 801px)');
    const time = mql.matches ? 18 : 9;
    const speed = distance / time;

    targets.forEach((target) => {
      const tickElems = target.querySelectorAll('.js-tick-item');
      if (!tickElems.length) {
        return;
      }

      const total = tickElems.length - 1;

      tickElems.forEach((el, i) => {
        const elWidth = el.clientWidth;
        const elTime = Math.floor(elWidth / speed);
        el.style.setProperty('--tick-duration', `${elTime}s`);
        el.style.setProperty('--tick-delay', `${elTime / -2}s`);

        if (i === total) {
          el.parentNode.classList.remove('no-tick');
        }
      });
    });
  }

  //テキストをコピーする
  copyText() {
    const targets = document.querySelectorAll('.js-tick');
    if (!targets.length) {
      return;
    }

    targets.forEach((target) => {
      const tickElems = target.querySelectorAll('.js-tick-item');
      if (!tickElems.length) {
        return;
      }

      let length = 0;
      tickElems.forEach((el) => {
        length += el.clientWidth;
        el.insertAdjacentHTML('afterend', el.outerHTML);
        if (length > window.innerWidth) {
          return;
        }
      });
    });
  }
}

new Main();