フレームワーク不要で始めるHTMLコンポーネント化


はじめに

HTMLを運用していると、最初は「ヘッダーをコピペ」「記事の注意書き枠をコピペ」「フッターも同じだからコピペ」で回りがちです。ところが、記事数やページ数が増えるほど、コピペ運用は確実に破綻します。

  • ナビに1リンク追加したいだけなのに、全ページを修正する羽目になる
  • 似たパーツが微妙に違う(余白、文言、リンク先)状態が増殖する
  • 直したつもりが、別ページだけ古いまま残って事故る
  • “どれが正”かわからなくなって改修が怖くなる

こういう「共通パーツ地獄」を抜けるために必要なのが、HTMLテンプレートの“コンポーネント化”です。
React/Vueのようなフレームワークを導入すればコンポーネント化は自然にできますが、静的HTMLや軽量な構成でも、同じ発想を取り入れられます。

この記事では、フレームワーク無しでも実現できる「崩れない共通パーツ」をテーマに、次の3つを軸に解説します。

  • <template>:HTMLの“設計図”を持つ
  • Web Components(Custom Elements + Shadow DOM):共通パーツを“タグ化”する
  • <slot>:外部から内容を差し込めるようにして“再利用性”を上げる

最後に、実際に 「注意枠(Callout)」「ページヘッダー」「コピーライト」をコンポーネント化して、複数ページで使い回すハンズオンをやります。


座学

1) 「コンポーネント化」とは何か(HTML運用の目標)

コンポーネント化は、単に「部品を作る」ことではありません。実務でのゴールはだいたいこの3点です。

  1. 単一の正(Single Source of Truth)
    ヘッダーやフッターの定義が1箇所にある状態。修正は1回で済む。
  2. インターフェース(使い方)が固定される
    「このタグを置けばヘッダーになる」「この属性を変えれば表示が変わる」という使い方が決まる。
    これが決まると、運用者が増えても事故が減る。
  3. HTMLの意味構造を壊しにくい
    共通枠の中身を勝手にいじってアクセシビリティやSEOを壊す事故が起きにくい。

フレームワークはこの仕組みを強制してくれますが、素のHTMLでも“同じ方向”に寄せることはできます。


2) <template> の正体:描画されないDOMの雛形

<template> は、ページ読み込み時点では 表示されない 特殊な要素です。中身はDOMとして保持されるので、JavaScriptから複製して挿入できます。

  • 使いどころ:同じ構造のパーツを複製したい(注意枠、カード、アイテム列など)
  • メリット:HTMLとして構造を管理できる(JSの文字列結合より安全)
  • 注意点:単体では再利用が弱い(“雛形”なので、使う側のJSが必要)

<template> は「部品の材料」、Web Componentsは「部品そのもの」と捉えると整理しやすいです。


3) Web Components:ブラウザ標準の“自作タグ”機構

Web Componentsはざっくり以下の集合です。

  • Custom Elements<my-header> のような自作タグを定義できる
  • Shadow DOM:内部構造とスタイルの影響範囲を閉じ込められる
  • HTML Templates:部品の雛形として使える
  • Slots:外から中身を差し込む(“穴”を作る)

特に重要なのは、「タグとして使える」ことです。
<site-header></site-header> と書くだけで共通ヘッダーが出るなら、コピペ運用から一気に抜けられます。


4) <slot>:部品を“固定化”しすぎないための差し込み口

コンポーネントは共通化すると便利ですが、共通化しすぎると「毎回ちょっと違う」要求に弱くなります。
そこで <slot> を用意して、使う側が差し込める場所を作ります。

  • slot なし:デフォルトの差し込み(本文など)
  • name="title":名前付きの差し込み(見出し、アクションボタン、アイコンなど)

この設計ができると、「構造は固定、内容は差し替え可能」になり、運用と拡張のバランスが良くなります。


5) Shadow DOMは使うべき?(結論:目的で決める)

Shadow DOMを使うと、コンポーネント内部のDOMとCSSを隔離できます。
ただし、使い方を誤ると「外からスタイルを当てられない」「デバッグしづらい」などの反作用もあります。

  • 隔離したい(壊されたくない):Shadow DOMあり
    例:UI部品、注意枠、ボタン、ナビなど
  • 外側のCSS設計に馴染ませたい:Shadow DOMなし(Light DOM)
    例:記事本文の構造、CMSテンプレなど

この記事のハンズオンでは、まず理解しやすいように Shadow DOMあり で「壊されにくい部品」を作ります。


ハンズオン

目的:フレームワーク無しで、以下の3部品を“タグ”として使えるようにします。

  • <site-header>:サイト共通ヘッダー(ナビ含む)
  • <callout-box>:注意枠(info/warnなど属性で切り替え)
  • <site-footer>:共通フッター

0) フォルダ構成

project/
  index.html
  about.html
  components/
    site-header.js
    site-footer.js
    callout-box.js

1) index.html(トップページ側の利用)

<!doctype html>
<html lang="ja">
<head>
  <meta charset="utf-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1" />
  <title>トップ | サンプルサイト</title>

  <!-- コンポーネント読み込み(defer推奨) -->
  <script src="./components/site-header.js" defer></script>
  <script src="./components/site-footer.js" defer></script>
  <script src="./components/callout-box.js" defer></script>
</head>
<body>
  <site-header current="home"></site-header>

  <main>
    <h1>トップページ</h1>

    <callout-box variant="info">
      <span slot="title">お知らせ</span>
      このサイトはWeb Componentsで共通パーツを管理しています。
    </callout-box>

    <p>本文が続きます。ページが増えてもヘッダーとフッターは一箇所の修正で反映できます。</p>
  </main>

  <site-footer></site-footer>
</body>
</html>

2) about.html(別ページでも同じ部品を使う)

<!doctype html>
<html lang="ja">
<head>
  <meta charset="utf-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1" />
  <title>About | サンプルサイト</title>

  <script src="./components/site-header.js" defer></script>
  <script src="./components/site-footer.js" defer></script>
  <script src="./components/callout-box.js" defer></script>
</head>
<body>
  <site-header current="about"></site-header>

  <main>
    <h1>About</h1>

    <callout-box variant="warn">
      <span slot="title">注意</span>
      callout-box は内部構造が固定されるので、運用で崩れにくいのがメリットです。
    </callout-box>

    <p>ページ固有の本文。</p>
  </main>

  <site-footer></site-footer>
</body>
</html>

3) components/site-header.js(共通ヘッダー)

  • current 属性で現在地を受け取り、該当リンクに aria-current="page" を付けます
  • Shadow DOMで内部を固定化(外から崩されにくい)
class SiteHeader extends HTMLElement {
  static get observedAttributes() {
    return ["current"];
  }

  constructor() {
    super();
    this.attachShadow({ mode: "open" });

    const template = document.createElement("template");
    template.innerHTML = `
      <style>
        /* 最小限の見た目(必要なら後で調整) */
        header { padding: 12px 16px; border-bottom: 1px solid #ddd; }
        nav ul { list-style: none; padding: 0; display: flex; gap: 12px; }
        a[aria-current="page"] { font-weight: bold; text-decoration: underline; }
      </style>
      <header>
        <div>サンプルサイト</div>
        <nav aria-label="グローバルナビ">
          <ul>
            <li><a data-key="home" href="./index.html">Home</a></li>
            <li><a data-key="about" href="./about.html">About</a></li>
          </ul>
        </nav>
      </header>
    `;
    this.shadowRoot.appendChild(template.content.cloneNode(true));
  }

  connectedCallback() {
    this.#applyCurrent();
  }

  attributeChangedCallback() {
    this.#applyCurrent();
  }

  #applyCurrent() {
    if (!this.shadowRoot) return;

    const current = this.getAttribute("current") || "";
    const links = this.shadowRoot.querySelectorAll("a[data-key]");
    links.forEach((a) => {
      const key = a.getAttribute("data-key");
      if (key === current) a.setAttribute("aria-current", "page");
      else a.removeAttribute("aria-current");
    });
  }
}

customElements.define("site-header", SiteHeader);

4) components/site-footer.js(共通フッター)

class SiteFooter extends HTMLElement {
  constructor() {
    super();
    this.attachShadow({ mode: "open" });

    const year = new Date().getFullYear();
    this.shadowRoot.innerHTML = `
      <style>
        footer { padding: 16px; border-top: 1px solid #ddd; margin-top: 24px; }
        small { color: #555; }
      </style>
      <footer>
        <small>&copy; ${year} サンプルサイト</small>
      </footer>
    `;
  }
}

customElements.define("site-footer", SiteFooter);

5) components/callout-box.js(注意枠:slotあり)

  • variant="info|warn" で役割を変える
  • <slot name="title"> で見出しを差し込める(指定がない場合はデフォルト表示)
class CalloutBox extends HTMLElement {
  static get observedAttributes() {
    return ["variant"];
  }

  constructor() {
    super();
    this.attachShadow({ mode: "open" });

    const template = document.createElement("template");
    template.innerHTML = `
      <style>
        .box { padding: 12px 14px; border-radius: 10px; border: 1px solid #ddd; }
        .title { margin: 0 0 6px; font-weight: bold; }
        .info { background: #f6fbff; border-color: #b9d7ff; }
        .warn { background: #fff8f2; border-color: #ffd2a6; }
      </style>
      <section class="box info" role="note">
        <p class="title"><slot name="title">補足</slot></p>
        <div><slot></slot></div>
      </section>
    `;
    this.shadowRoot.appendChild(template.content.cloneNode(true));
  }

  connectedCallback() {
    this.#applyVariant();
  }

  attributeChangedCallback() {
    this.#applyVariant();
  }

  #applyVariant() {
    const variant = (this.getAttribute("variant") || "info").toLowerCase();
    const section = this.shadowRoot?.querySelector("section");
    if (!section) return;

    section.classList.remove("info", "warn");
    section.classList.add(variant === "warn" ? "warn" : "info");
  }
}

customElements.define("callout-box", CalloutBox);

6) 動作確認(ここが“運用設計”に効く)

  1. index.html を開く → ヘッダーとフッターが表示される
  2. about.html を開く → 同じヘッダー/フッターが再利用される
  3. site-header.js のリンク文言を変える → 両ページに反映される
  4. callout-boxvariant を変える → 表現が変わる
  5. slot="title" を外す → タイトルがデフォルト「補足」になる

ここまでできると、共通パーツが“タグ化”され、コピペの破綻ポイントをかなり潰せます。


まとめ

HTMLテンプレートのコンポーネント化は、フレームワーク導入の有無に関係なく、運用を安定させるための設計技術です。<template> を雛形として持ち、Web Componentsでタグ化し、<slot> で差し込み可能にすることで、

  • 変更を1箇所に集約できる(共通パーツのSingle Source化)
  • 使い方が固定される(運用者が増えても事故が減る)
  • HTML構造が崩れにくい(アクセシビリティの土台が守られる)

という実務メリットが得られます。

特に、ブログやドキュメントのようにページ数が増えるコンテンツは、早めに共通パーツを部品化しておくと、後半で効いてきます。まずは「ヘッダー・フッター・注意枠」など、差分が少なく効果が大きいところから始めるのがコツです。


投稿日

カテゴリー:

投稿者:

タグ:

コメント

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です