はじめに
HTMLを運用していると、最初は「ヘッダーをコピペ」「記事の注意書き枠をコピペ」「フッターも同じだからコピペ」で回りがちです。ところが、記事数やページ数が増えるほど、コピペ運用は確実に破綻します。
- ナビに1リンク追加したいだけなのに、全ページを修正する羽目になる
- 似たパーツが微妙に違う(余白、文言、リンク先)状態が増殖する
- 直したつもりが、別ページだけ古いまま残って事故る
- “どれが正”かわからなくなって改修が怖くなる
こういう「共通パーツ地獄」を抜けるために必要なのが、HTMLテンプレートの“コンポーネント化”です。
React/Vueのようなフレームワークを導入すればコンポーネント化は自然にできますが、静的HTMLや軽量な構成でも、同じ発想を取り入れられます。
この記事では、フレームワーク無しでも実現できる「崩れない共通パーツ」をテーマに、次の3つを軸に解説します。
<template>:HTMLの“設計図”を持つ- Web Components(Custom Elements + Shadow DOM):共通パーツを“タグ化”する
<slot>:外部から内容を差し込めるようにして“再利用性”を上げる
最後に、実際に 「注意枠(Callout)」「ページヘッダー」「コピーライト」をコンポーネント化して、複数ページで使い回すハンズオンをやります。
座学
1) 「コンポーネント化」とは何か(HTML運用の目標)
コンポーネント化は、単に「部品を作る」ことではありません。実務でのゴールはだいたいこの3点です。
- 単一の正(Single Source of Truth)
ヘッダーやフッターの定義が1箇所にある状態。修正は1回で済む。 - インターフェース(使い方)が固定される
「このタグを置けばヘッダーになる」「この属性を変えれば表示が変わる」という使い方が決まる。
これが決まると、運用者が増えても事故が減る。 - 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>© ${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) 動作確認(ここが“運用設計”に効く)
index.htmlを開く → ヘッダーとフッターが表示されるabout.htmlを開く → 同じヘッダー/フッターが再利用されるsite-header.jsのリンク文言を変える → 両ページに反映されるcallout-boxのvariantを変える → 表現が変わるslot="title"を外す → タイトルがデフォルト「補足」になる
ここまでできると、共通パーツが“タグ化”され、コピペの破綻ポイントをかなり潰せます。
まとめ
HTMLテンプレートのコンポーネント化は、フレームワーク導入の有無に関係なく、運用を安定させるための設計技術です。<template> を雛形として持ち、Web Componentsでタグ化し、<slot> で差し込み可能にすることで、
- 変更を1箇所に集約できる(共通パーツのSingle Source化)
- 使い方が固定される(運用者が増えても事故が減る)
- HTML構造が崩れにくい(アクセシビリティの土台が守られる)
という実務メリットが得られます。
特に、ブログやドキュメントのようにページ数が増えるコンテンツは、早めに共通パーツを部品化しておくと、後半で効いてきます。まずは「ヘッダー・フッター・注意枠」など、差分が少なく効果が大きいところから始めるのがコツです。
コメントを残す