💈

Web Componentsでmarqueeを作る

2024/12/29に公開

Web Componentsという仕様を知っていますか?
独自のHTML要素に近いものをJavaScriptで作ってみます.

ちなみにWeb Componentsの説明についてはMDN Web Docsにて「再利用可能なカスタム要素を作成し、その機能を他のコードから分離してウェブアプリケーションで利用できるようにします。」と書かれています.

https://developer.mozilla.org/ja/docs/Web/API/Web_components

これらの仕様と機能を使って、<marquee>要素を実装します.

<marquee>要素については以下のMDN Web Docsを参照してください.
https://developer.mozilla.org/ja/docs/Web/HTML/Element/marquee

今回作るWeb Componentsについて

今回は<marquee>要素を作りたいのですが、Web Componentsの仕様上、要素名には-を含む必要があるため<marquee-x>という名前で要素を作ります.

以下のようなイメージで使える要素を目指していきます.

<marquee-x speed="5"> Hello </marquee-x>

Web ComponentsはHTMLElementを継承したClassをwindow.customElements.define('marquee-x', MarqueeX);の記述で要素を有効化します.

class MarqueeX extends HTMLElement {
    constructor() {
        super();
    }

    connectedCallback() {
    }
}
window.customElements.define('marquee-x', MarqueeX);

タグで囲まれた要素を表示したい

タグで囲まれた要素を表示するにあたって<slot>を利用します.
<slot>の詳細な仕様の説明は避けますが、カスタムコンポーネントで囲んだ要素をWeb Componentsの中で表示できるようになります.

<slot>要素内に<span>タグを置くことで今回はアニメーションやスタイルの適用をやりやすくします.

const slotElement = document.createElement('slot');
const wrapperElement = document.createElement('span');
wrapperElement.innerHTML = slotElement.outerHTML;

スタイルを設定したい

<slot>と対になる<template>タグを利用し<template>内に、Web Components内で使いたいHTMLタグを記述します. styleの適用先として:slottedの導入を検討しましたが、要素内にrawTextしかない場合にスタイルの適用がされないため, <span>タグに対してテキストを当てます.

うまく動かない.js
const templateElement = document.createElement('template');

templateElement.innerHTML = `
<style>
      :host {
        white-space: nowrap;
        margin: 0 calc(50% - 50vw);
        width: 100vw;
        position: relative;
      }

      ::slotted(*) {
        position: absolute;
        left: 100vw;
        position: relative;
        display: inline-block;
      }
</style>
`;

```javascript: うまく動く.js

const templateElement = document.createElement('template');

	templateElement.innerHTML = `
    <style>
      :host {
        white-space: nowrap;
        margin: 0 calc(50% - 50vw);
        width: 100vw;
        position: relative;
      }

      span {
        position: absolute;
        left: 100vw;
      }

      span::slotted(*) {
        position: relative;
        display: inline-block;
      }
    </style>
  `;

それぞれ<slot>と<template>の詳しい仕様や解説はMDN Web Docsに説明を譲ります.
https://developer.mozilla.org/ja/docs/Web/API/Web_components/Using_templates_and_slots

JavaScriptで動かす

<slot>で呼び出された要素を<slot>で囲み、<span>タグ経由でJavaScriptのanimate()を呼び出します.

wrapperElement.id = 'animate-target';
...

		connectedCallback() {
			const animateTarget = this.shadowRoot?.getElementById('animate-target');
			if (animateTarget) {
				const keyframes = [{ left: '100vw' }, { left: '0' }];

				const options = {
					duration: window.innerWidth ? (window.innerWidth / this.speed) * 250 : 10000,
					iterations: Infinity
				};
				animateTarget.animate(keyframes, options);
			}
		}

最終的なWeb Components

厳密に仕様を再現しているわけでも、レスポンシブ対応しているわけでもないですが、一旦marqueeっぽく動くオレオレ実装での独自要素ができました.

最終的なソースコードは以下の通りです.

<script>
	const templateElement = document.createElement('template');

	templateElement.innerHTML = `
    <style>
      :host {
        white-space: nowrap;
        margin: 0 calc(50% - 50vw);
        width: 100vw;
        position: relative;
      }

      span {
        position: absolute;
        left: 100vw;
      }

      span::slotted(*) {
        position: relative;
        display: inline-block;
      }
    </style>
  `;

	const slotElement = document.createElement('slot');
	const wrapperElement = document.createElement('span');
	wrapperElement.innerHTML = slotElement.outerHTML;

	wrapperElement.id = 'animate-target';

	class MarqueeX extends HTMLElement {
		speed: number;

		constructor() {
			super();
			this.attachShadow({ mode: 'open' });
			if (this.shadowRoot) {
				this.shadowRoot.appendChild(templateElement.content.cloneNode(true));
				this.shadowRoot.appendChild(wrapperElement);
			}

			const speedAttribute = this.getAttribute('speed');

			this.speed = speedAttribute ? parseInt(speedAttribute) : 5;
		}

		connectedCallback() {
			const animateTarget = this.shadowRoot?.getElementById('animate-target');
			if (animateTarget) {
				const keyframes = [{ left: '100vw' }, { left: '0' }];

				const options = {
					duration: window.innerWidth ? (window.innerWidth / this.speed) * 250 : 10000,
					iterations: Infinity
				};
				animateTarget.animate(keyframes, options);
			}
		}
	}
	window.customElements.define('marquee-x', MarqueeX);
</script>

<marquee-x speed="5"> Hello </marquee-x>

動いている様子

デモ環境

例に漏れず環境はSvelte-Kit上ですが、生のJavaScriptが動いています.

https://web-api-dev.pages.dev/marquee-webcomponent

Discussion