innerHTMLとtextContentの違いは、文字列を「HTMLとして解釈するか」「ただの文字として扱うか」です。innerHTMLはタグを解釈して要素を作り、textContentはタグも含めてそのまま文字として表示します。innerTextは、画面に見えている文字だけを取り出すための別のプロパティです。 文字を表示するだけならtextContent、タグを含むHTMLを組み立てたいときだけinnerHTML系を使う、と決めておくと迷いません。

この記事では、同じHTMLに対して3つのプロパティで取得・代入した結果を並べて比較します。さらに、innerHTMLにユーザー入力をそのまま入れるとスクリプトが動く様子(XSS)を再現し、textContent・insertAdjacentHTML・setHTML(Sanitizer API)・DOMPurifyを使った安全な書き換え方を、主要ブラウザーの対応状況つきでまとめました。実行結果は Chromium 141(Playwright 1.56)で実際に動かして取得しています。

innerHTML・textContent・innerTextの早見表

3つの違いを1枚にまとめた比較表です。 各行の根拠となる実行結果は、このあとの章に載せています。

比べる点 innerHTML textContent innerText
取得できるもの 子孫のHTML文字列(タグ込み) 子孫のテキストすべて 画面に表示されているテキスト
代入した文字列のタグ 解釈される(要素になる) 解釈されない(文字として表示) 解釈されない(文字として表示)
display:none の要素(取得時) 含む 含む 含まない
visibility:hidden の要素(取得時) 含む 含む 含まない
script・styleの中身(取得時) 含む 含む 含まない
連続する空白・改行(取得時) ソースのまま ソースのまま 表示に合わせて1つにまとまる
<br>(取得時) <br> のまま 何も出ない 改行(\n)になる
CSSの text-transform(取得時) 反映されない 反映されない 反映される(大文字化など)
改行を含む文字列の代入 改行文字のまま 改行文字のまま <br> 要素に変換される
& < > の扱い(代入時) そのまま解釈(エスケープは自分で行う) 自動でエスケープ 自動でエスケープ
速度(取得) 中 速い レイアウト計算が入るぶん時間がかかる
外から来た文字列を入れたときの安全性 対策が必要(スクリプトが動くことがある) 安全 安全
使えるノード Element すべてのNode HTMLElement
主な用途 信頼できるHTMLの組み立て 文字の表示・取得 見た目どおりの文字をコピーしたいとき

選び方の結論は次のとおりです。

やりたいこと 使うもの
文字を表示する・書き換える el.textContent = 文字列
要素内のテキストをすべて取得する el.textContent
画面に見えている文字だけを取得する el.innerText
自分で書いた固定のHTMLを入れる el.innerHTML = '<b>固定のHTML</b>'
既存の中身を残してHTMLを追加する el.insertAdjacentHTML('beforeend', html)
外から来たHTML文字列を表示する el.setHTML(html) または el.innerHTML = DOMPurify.sanitize(html)

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「innerHTMLとtextContentの違い|innerText比較表」

同じHTMLから3つで取得した結果を比べる

まず「取得」の違いです。次のHTMLに対して、3つのプロパティを読み取りました。太字、非表示の段落、<br>、大文字化するCSS、style、script、エスケープされた文字を1つの div に入れています。

<style>.hide{display:none}.up{text-transform:uppercase}</style>
<div id="box">
  <p>こんにちは   <b>太字</b>です</p>
  <p class="hide">非表示の段落</p>
  1行目<br>2行目
  <span class="up">upper</span>
  <style>.x{color:red}</style>
  <script>window.ran = 1;</script>
  &lt;エスケープ&gt; &amp;
</div>
const box = document.getElementById('box');
console.log(box.innerHTML);
console.log(box.textContent);
console.log(box.innerText);

innerHTMLの取得結果


  <p>こんにちは   <b>太字</b>です</p>
  <p class="hide">非表示の段落</p>
  1行目<br>2行目
  <span class="up">upper</span>
  <style>.x{color:red}</style>
  <script>window.ran = 1;</script>
  &lt;エスケープ&gt; &amp;

タグも空白もソースのまま返ります。&lt; などのエスケープも、エスケープされた形のままです。

textContentの取得結果


  こんにちは   太字です
  非表示の段落
  1行目2行目
  upper
  .x{color:red}
  window.ran = 1;
  <エスケープ> &

タグだけを取り除いたテキストがすべて返ります。display:none の段落も、styleやscriptの中身も含まれます。<br> は何も出力されないため「1行目2行目」とつながり、&lt; は < に戻ります。

innerTextの取得結果

こんにちは 太字です

1行目
2行目 UPPER <エスケープ> &

画面に表示されている見た目に近い文字が返ります。非表示の段落、style、scriptは含まれず、連続した空白は1つにまとまり、<br> は改行になり、text-transform: uppercase を当てた「upper」は「UPPER」になっています。段落(p要素)の後ろには空行が入ります。

取得結果の違いを項目別に整理

HTMLの中身 innerHTML textContent innerText
<b>太字</b> <b>太字</b> 太字 太字
連続する空白3つ 3つのまま 3つのまま 1つにまとまる
<p class="hide">(display:none) 含む 含む 含まない
<br> <br> 出力なし 改行
text-transform: uppercase の「upper」 upper upper UPPER
<style> の中身 含む 含む 含まない
<script> の中身 含む 含む 含まない
&lt;エスケープ&gt; &lt;エスケープ&gt; <エスケープ> <エスケープ>

非表示の指定ごとのinnerTextの違い

「見えていない」指定にはいくつか種類があります。5つの span を並べて確かめました。

el.innerHTML = 'A<span style="visibility:hidden">B</span>'
  + '<span style="display:none">C</span><span hidden>D</span>'
  + '<span style="opacity:0">E</span>';
console.log(el.textContent); // ABCDE
console.log(el.innerText);   // AE
指定 textContent innerText
visibility:hidden 含む 含まない
display:none 含む 含まない
hidden 属性 含む 含まない
opacity:0 含む 含む(透明でも描画されている扱い)

もう1つ知っておきたいのが、画面に描画されていない要素のinnerTextはtextContentと同じ結果になる点です。document.createElement で作っただけでページに追加していない要素では、非表示の指定が効きません。

const d = document.createElement('div'); // まだページに追加していない
d.innerHTML = '<p>a</p><p class="hide">b</p>';
console.log(JSON.stringify(d.innerText));   // "ab"
console.log(JSON.stringify(d.textContent)); // "ab"

同じ文字列を3つで代入した結果を比べる

次は「代入」の違いです。タグ・改行・& を含む同じ文字列を、3つのプロパティでそれぞれ代入しました。

const src = '<b>太字</b>と\n改行 & <i>斜体</i>';

t1.innerHTML = src;
t2.textContent = src;
t3.innerText = src;

代入後の要素を調べた結果です。

代入に使ったもの 代入後のHTML(el.innerHTMLで確認) 子ノード数 b要素の数 画面の表示
innerHTML <b>太字</b>と(改行)改行 &amp; <i>斜体</i> 3 1 太字と斜体が効いた状態で1行に表示
textContent &lt;b&gt;太字&lt;/b&gt;と(改行)改行 &amp; &lt;i&gt;斜体&lt;/i&gt; 1 0 <b>太字</b>と 改行 & <i>斜体</i>(1行)
innerText &lt;b&gt;太字&lt;/b&gt;と<br>改行 &amp; &lt;i&gt;斜体&lt;/i&gt; 3 0 <b>太字</b>と(改行)改行 & <i>斜体</i>(2行)

読み取れるポイントは3つです。

  1. innerHTMLだけがタグを解釈し、b要素とi要素が実際に作られます。
  2. textContentとinnerTextは < > & を自動でエスケープします。タグは文字として画面に表示されます。
  3. 改行文字(\n)を <br> に変えるのはinnerTextだけです。textContentは改行文字をそのままテキストノードに入れるので、通常のCSSでは空白1つ分として表示されます。

textContentで改行を表示する方法

textContentで入れた改行を画面でも改行させたいときは、CSSの white-space: pre-line(または pre-wrap)を指定します。

.message { white-space: pre-line; }
el.textContent = '1行目\n2行目'; // HTMLは「1行目(改行)2行目」のまま、表示は2行になる

pre 要素に代入したときの違いも確かめました。textContentは a(改行)b のままテキストノード1つ、innerTextは a<br>b とbr要素が作られます。ログやコードを pre に表示するならtextContentが向いています。

nullやundefinedを代入したとき

代入 代入後の中身
el.textContent = null 空("")
el.textContent = undefined 空("")
el.innerHTML = null 空("")
el.innerHTML = undefined 文字列の "undefined" が表示される

innerHTML = undefined だけは "undefined" という文字が画面に出ます。値がないかもしれない変数を表示するときは、el.textContent = value ?? '' のように既定値を添えると表示がそろいます。

innerHTMLはHTMLとして整えられてから入る

innerHTMLに代入した文字列は、ブラウザーのHTMLパーサーを通ります。そのため、書いた文字列と、あとで読み出した文字列が同じになるとは限りません。

代入した文字列 代入後の innerHTML 何が起きたか
<td>セル</td><div>ブロック(p要素へ) セル<div>ブロック</div> 置けない場所のtdは外され、閉じていないdivは閉じられる
<tr><td>1</td></tr>(table要素へ) <tbody><tr><td>1</td></tr></tbody> tbodyが自動で補われる
改行 & <i>斜体</i> 改行 &amp; <i>斜体</i> & が &amp; に直される

速度の違いを計測した結果

2,000行のリスト(各行に span と b を含む div)を使って、取得と代入の時間を計測しました。Chromium 141 のヘッドレス実行での値で、端末やブラウザーによって変わります。大小関係の目安としてご覧ください。

const bench = (f, n) => {
  const t = performance.now();
  for (let i = 0; i < n; i++) f();
  return +(performance.now() - t).toFixed(1);
};
// 取得:2,000行を含む親要素から200回読み取る
bench(() => { sink += host.textContent.length; }, 200);
// 代入:2,000個のspanに同じ文字列を入れる処理を20回
bench(() => { for (const c of cells) c.textContent = 'テキスト'; }, 20);
操作 textContent innerText innerHTML
取得(2,000行の親から200回) 30.6ms 270.2ms 155.2ms
代入(2,000個のspan × 20回)1回目の順番 33.1ms 29.5ms 61.4ms
代入(同上)順番を入れ替えて再計測 29.2ms 48.0ms 69.4ms
  • 取得はtextContentが最も速く、innerTextはその約9倍の時間がかかりました。innerTextは「どの文字が見えているか」を知るためにスタイルとレイアウトの計算が必要になるためです。
  • 代入はinnerHTMLがtextContentの約2倍でした。HTMLパーサーを通すぶんの差です。innerTextの代入は計測の順番で30〜48msと幅があり、textContentと同程度からやや遅い範囲でした。
  • 数件の書き換えでは体感できる差にはなりません。ループの中で大量の要素から読み取る処理では、textContentを選ぶと効果が出やすい、という理解で十分です。

innerHTMLにユーザー入力を入れるとスクリプトが動く例

innerHTMLは文字列をHTMLとして解釈するので、入力欄やURL、APIから届いた文字列をそのまま入れると、その中のタグや属性も解釈されます。 属性の中にスクリプトが書かれていれば、それも動きます。これがクロスサイトスクリプティング(XSS)と呼ばれる状態です。

確認用に、次のHTMLを用意しました。入力欄には、最初から「タグを含む文字列」を入れてあります。スクリプトが動いたことがわかるように、動いた場合はページのタイトルを書き換えるだけの無害な内容にしています。

<!DOCTYPE html>
<html lang="ja">
<head><meta charset="utf-8"><title>表示名の確認</title></head>
<body>
  <input id="name" size="60">
  <button id="unsafe">innerHTMLで表示</button>
  <button id="safe">textContentで表示</button>
  <p>こんにちは、<span id="out"></span> さん</p>
  <script>
    const input = document.getElementById('name');
    const out = document.getElementById('out');
    // 入力欄に「タグを含む文字列」が入った状態を再現
    input.value = '<img src="x" onerror="document.title=\'入力した文字列がスクリプトとして動きました\'">';

    document.getElementById('unsafe').addEventListener('click', () => {
      out.innerHTML = input.value;   // 文字列がHTMLとして解釈される
    });
    document.getElementById('safe').addEventListener('click', () => {
      out.textContent = input.value; // 文字列がそのまま文字として表示される
    });
  </script>
</body>
</html>

それぞれのボタンを押したあとの状態を、Playwrightで確認した結果です。

押したボタン ページのタイトル #out内のimg要素 画面に見える文字
innerHTMLで表示 入力した文字列がスクリプトとして動きました 1個 (なし)
textContentで表示 表示名の確認(変化なし) 0個 <img src="x" onerror="..."> がそのまま文字として表示

innerHTMLのほうでは、入力した文字列からimg要素が作られ、画像の読み込みエラーをきっかけに onerror の中身が実行されました。 textContentのほうでは、同じ文字列がただの文字として表示されるだけです。

「scriptタグは動かない」だけでは防げない

innerHTMLで入れた <script> は実行されない、という仕様があります。実際に確かめた結果が次の表です。

innerHTMLに入れた文字列 結果
<script>window.hit.push("script")</script> 実行されない(要素は作られる)
<img src="x" onerror="window.hit.push('img onerror')"> 実行される
<a href="javascript:...">link</a> リンクとして残る(クリックすると実行される形)

scriptタグが動かなくても、イベントハンドラー属性(onerror、onclickなど)や javascript: で始まるリンクは有効です。「scriptという文字を取り除く」といった自作の置換では抜けが出やすいので、次の章の方法を使います。

なお、XSSの全体像(保存型・反射型の違い、サーバー側の対策、Content Security Policyなど)は範囲が広いので、この記事ではDOMの書き換えに関わる部分に絞っています。

安全に書き換える方法の比較表

外から来た文字列を表示するときの選択肢を、対応状況とあわせてまとめました。対応バージョンはMDNのブラウザー対応データ(npmパッケージ @mdn/browser-compat-data 8.1.4、2026-10-01版)で確認した値です。

方法 タグの扱い 外から来た文字列 Chrome / Edge Firefox Safari
el.textContent = s 文字として表示 そのまま渡してよい 1 / 12 1 3
el.insertAdjacentText(位置, s) 文字として追加 そのまま渡してよい 1 / 17 48 4
document.createElement + append 要素を1つずつ作る そのまま渡してよい(textContentで入れる) 1 / 12 1 1
el.innerHTML = s 解釈する エスケープかサニタイズが必要 1 / 12 1 1
el.insertAdjacentHTML(位置, s) 解釈して追加 エスケープかサニタイズが必要 1 / 17 8 4
el.setHTML(s)(Sanitizer API) 解釈し、スクリプトにつながる部分を除く そのまま渡してよい 146 / 146 148 未対応
el.setHTMLUnsafe(s) 解釈する(除去はしない) エスケープかサニタイズが必要 124 / 124 123 26
el.innerHTML = DOMPurify.sanitize(s) 解釈し、スクリプトにつながる部分を除く そのまま渡してよい ライブラリーが対応 ライブラリーが対応 ライブラリーが対応

setHTMLとsetHTMLUnsafe以外は、どのブラウザーでも長く使える機能です。createElementの行はcreateElement自体の対応バージョンを載せています。

flowchart TD
  A[要素の中身を書き換えたい] --> B{タグを含むHTMLとして表示したい?}
  B -- いいえ --> C["textContent"]
  B -- はい --> D{文字列は自分で書いた固定のHTML?}
  D -- はい --> E["innerHTML / insertAdjacentHTML"]
  D -- いいえ --> F{差し込むのは値だけ?}
  F -- はい --> G["createElement + textContent または エスケープ関数"]
  F -- いいえ --> H["setHTML または DOMPurify.sanitize"]

方法1:textContentで文字として入れる

最も確実で、最も速い方法です。表示したいのが文字だけなら、これで完結します。

x.textContent = '<img src="x" onerror="window.hit.push(1)">';
確認した項目 結果
代入後のHTML &lt;img src="x" onerror="window.hit.push(1)"&gt;
作られたimg要素 0個
スクリプトの実行 なし

要素の途中に文字を追加したいときは insertAdjacentText が使えます。こちらも同じく文字として追加され、スクリプトは実行されませんでした。

方法2:createElementとtextContentでリストを組み立てる

タグの構造は自分で作り、値の部分だけをtextContentで入れる方法です。値にタグが含まれていても、文字として表示されます。

const items = [
  { name: '<b>ノート</b>', qty: 3 },      // 値にタグが混ざっている想定
  { name: 'ペン & 消しゴム', qty: 12 },
];
const list = document.getElementById('list'); // <ul id="list"></ul>

list.replaceChildren(...items.map(it => {
  const li = document.createElement('li');
  li.textContent = `${it.name}:${it.qty}個`;
  return li;
}));
console.log(list.innerHTML);
<li>&lt;b&gt;ノート&lt;/b&gt;:3個</li><li>ペン &amp; 消しゴム:12個</li>

replaceChildren は、既存の子要素をすべて取り除いて新しい子要素に入れ替えるメソッドです(Chrome 86、Firefox 78、Safari 14 以降)。構造が複雑なときは template 要素にひな形を書き、複製してから値を入れると読みやすくなります。

const tpl = document.createElement('template');
tpl.innerHTML = '<li class="item"><span class="name"></span> <em class="qty"></em></li>'; // 固定のHTML

const node = tpl.content.cloneNode(true);
node.querySelector('.name').textContent = items[0].name;
node.querySelector('.qty').textContent = items[0].qty;
list.append(node);
console.log(list.lastElementChild.outerHTML);
<li class="item"><span class="name">&lt;b&gt;ノート&lt;/b&gt;</span> <em class="qty">3</em></li>

方法3:エスケープしてからinnerHTMLに入れる

テンプレート文字列でHTMLをまとめて書きたいときは、差し込む値をエスケープ関数に通します。 & < > " ' の5文字を文字参照に置き換える関数です。

const escapeHTML = s => String(s).replace(/[&<>"']/g, c => ({
  '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;',
}[c]));

card.innerHTML = items.map(it =>
  `<p class="item"><strong>${escapeHTML(it.name)}</strong> ${escapeHTML(it.qty)}個</p>`
).join('');
console.log(card.innerHTML);
<p class="item"><strong>&lt;b&gt;ノート&lt;/b&gt;</strong> 3個</p><p class="item"><strong>ペン &amp; 消しゴム</strong> 12個</p>

この方法は、値をタグの間のテキストか引用符で囲んだ属性値に入れる場合に使えます。href や src に値を入れる場合は、エスケープに加えて、URLが https: などで始まるかの確認も必要です。差し込む場所が増えるほど通し忘れが起きやすいので、方法2や方法5と組み合わせると安心です。

方法4:insertAdjacentHTMLで中身を残して追加する

insertAdjacentHTML は、既存の中身を残したまま、指定した位置にHTMLを追加するメソッドです。文字列をHTMLとして解釈する点はinnerHTMLと同じなので、外から来た文字列を渡すときはエスケープかサニタイズが必要です(img要素のonerrorが実行されることを確認しました)。

位置は4種類あります。

s.innerHTML = '<p id="mid">中</p>';
const mid = document.getElementById('mid');
mid.insertAdjacentHTML('beforebegin', '<i>1</i>'); // 要素の直前
mid.insertAdjacentHTML('afterbegin',  '<i>2</i>'); // 要素の中の先頭
mid.insertAdjacentHTML('beforeend',   '<i>3</i>'); // 要素の中の末尾
mid.insertAdjacentHTML('afterend',    '<i>4</i>'); // 要素の直後
console.log(s.innerHTML);
<i>1</i><p id="mid"><i>2</i>中<i>3</i></p><i>4</i>

innerHTML += との違い

追加のつもりで innerHTML += と書くと、既存の中身がいったん文字列に戻され、全体が作り直されます。 その結果、すでに付けていたイベントリスナーが外れます。

x.innerHTML = '<button id="btn">押す</button>';
let count = 0;
document.getElementById('btn').addEventListener('click', () => count++);
document.getElementById('btn').click();      // count は 1

x.innerHTML += '<span>追加</span>';           // 全体が作り直される
document.getElementById('btn').click();      // 新しいボタンにはリスナーがない
console.log(count);                          // 1 のまま
追加の書き方 追加後にボタンを押した回数の記録 ボタン要素
x.innerHTML += '<span>追加</span>' 増えない(リスナーが外れる) 別の要素に作り直される
x.insertAdjacentHTML('beforeend', '<span>追加</span>') 増える(リスナーは残る) 同じ要素のまま

追加にはinsertAdjacentHTMLかappendを使うと、既存の要素とリスナーを保ったまま追記できます。

方法5:setHTML(Sanitizer API)で安全に解釈させる

Element.setHTML() は、HTMLとして解釈しつつ、スクリプトの実行につながる要素や属性を取り除いてから挿入するブラウザー標準のメソッドです。MDNのsetHTMLのページでは、ユーザーが用意したHTML文字列を入れる場面で、innerHTMLの置き換えとして使うことがすすめられています。

// 既定の設定:script要素やイベントハンドラー属性などが取り除かれる
target.setHTML('<p>こんにちは<img src="x" onerror="alert(1)"></p>');

// 許可する要素を自分で決める場合
const sanitizer = new Sanitizer({ elements: ['p', 'b', 'a'] });
target.setHTML(userHtml, { sanitizer });

MDNによると、script・frame・iframe・embed・object・use の各要素とイベントハンドラー属性は、設定で許可しても常に取り除かれます。

対応状況は Chrome 146以降、Edge 146以降、Firefox 148以降で、Safariは未対応です(@mdn/browser-compat-data 8.1.4)。MDN上の分類も「Limited availability(一部のブラウザーで未対応)」です。今回の検証環境は Chromium 141 のため typeof Element.prototype.setHTML は "undefined" で、上のsetHTMLのコードは未実行です。未対応のブラウザーでも動かすには、次のように使えるかを確かめてから呼び、使えない場合はDOMPurifyに切り替えます。

function setSafeHTML(el, html) {
  if (typeof el.setHTML === 'function') {
    el.setHTML(html);                        // 対応ブラウザー
  } else {
    el.innerHTML = DOMPurify.sanitize(html); // それ以外
  }
}

名前の似た setHTMLUnsafe() は、Chrome 124・Firefox 123・Safari 26 以降で使えますが、取り除く処理を行わないメソッドです。Chromium 141 で実行したところ、img要素のonerrorが実行されました。宣言的シャドウDOMを含むHTMLを入れるためのもので、安全性の面ではinnerHTMLと同じ扱いになります。

方法6:DOMPurifyでサニタイズしてから入れる

DOMPurifyは、HTML文字列からスクリプトの実行につながる部分を取り除くライブラリーです。すべての主要ブラウザーで同じ結果が得られるので、setHTMLが使えない環境も含めて対応したいときに向いています。今回は npm で公開されている最新版 3.4.16 を使いました。

npm install dompurify
import DOMPurify from 'dompurify';

const dirty = '<p>お知らせ<img src="x" onerror="alert(1)"><script>alert(2)</script> '
  + '<a href="javascript:alert(3)">リンク</a> '
  + '<a href="https://example.com" target="_blank">外部</a> <b onclick="x()">太字</b></p>';

console.log(DOMPurify.sanitize(dirty));
console.log(DOMPurify.sanitize(dirty, { ALLOWED_TAGS: ['p', 'b', 'a'], ALLOWED_ATTR: ['href'] }));
console.log(DOMPurify.sanitize(dirty, { ALLOWED_TAGS: [] }));

実行結果(Chromium 141、DOMPurify 3.4.16)は次のとおりです。

<p>お知らせ<img src="x"> <a>リンク</a> <a href="https://example.com">外部</a> <b>太字</b></p>
<p>お知らせ <a>リンク</a> <a href="https://example.com">外部</a> <b>太字</b></p>
お知らせ リンク 外部 太字
入力に含まれていたもの 既定の設定での結果
onerror 属性 取り除かれる(img要素は残る)
<script> 要素 要素ごと取り除かれる
href="javascript:..." href属性が取り除かれる(a要素は残る)
onclick 属性 取り除かれる
target="_blank" 取り除かれる
href="https://example.com" 残る

サニタイズ後の文字列をinnerHTMLに入れて、スクリプトが実行されないことも確認しました。2つ目の例のように 許可するタグと属性を絞ると、表示したい範囲だけを通せます。3つ目のように ALLOWED_TAGS: [] とすると、タグをすべて外したテキストだけが得られます。

Trusted Typesで書き方をそろえる

Trusted Typesは、innerHTMLなどに「ただの文字列」を渡せないようにし、決められた関数を通した値だけを受け付けるようにするブラウザーの仕組みです。Content-Security-Policyヘッダーに require-trusted-types-for 'script' を指定して有効にします。MDNのデータでは Chrome 83、Firefox 148、Safari 26 以降で対応しています(Chromium 141 で typeof window.trustedTypes が "object" であることは確認しましたが、ヘッダーを付けた動作は未実行です)。チーム全体で「innerHTMLには必ずサニタイズ済みの値を渡す」というルールを仕組みでそろえたいときに役立ちます。

よくあるつまずきと直し方

書いたコード 起きること 直し方
el.innerHTML = 入力値 入力値のタグが解釈され、属性内のスクリプトが動くことがある el.textContent = 入力値
el.innerHTML += html 既存の要素が作り直され、リスナーが外れる el.insertAdjacentHTML('beforeend', html)
el.textContent = 'a<br>b' <br> が文字として表示される 改行文字 \n を入れて white-space: pre-line を指定
el.textContent = '1行目\n2行目' 1行で表示される white-space: pre-line を指定、または innerText で代入
大量の要素に innerText で読み取り レイアウト計算が毎回走り、時間がかかる textContent で読み取る
非表示要素の文字も innerText で取ろうとする 取得できない textContent で取得する
el.innerHTML = undefined "undefined" と表示される value ?? '' を代入する
table.innerHTML = '<tr>...' 後に table.firstChild を見る trではなくtbodyが返る table.querySelector('tr') で探す
el.innerHTML = '<script>...</script>' スクリプトは実行されない document.createElement('script') で作って追加する
el.innerHTML = '' で中身を空にする 動くが、意図が伝わりにくい el.replaceChildren() または el.textContent = ''

Reactなどのフレームワークでの対応

フレームワークでも考え方は同じです。既定の書き方はtextContent相当(自動でエスケープ)で、HTMLとして入れる書き方は名前で区別されています。 以下は各フレームワークの一般に知られた仕様で、今回の検証では未実行です。

フレームワーク 文字として表示(既定) HTMLとして挿入
React {value} dangerouslySetInnerHTML={{ __html: html }}
Vue {{ value }} v-html="html"
Svelte {value} {@html html}
Angular {{ value }} [innerHTML]="html"(既定でサニタイズが入る)

HTMLとして挿入する書き方を使うときは、素のJavaScriptと同じく、DOMPurifyなどでサニタイズした文字列を渡します。

よくある質問

Q. innerTextとtextContentはどちらを使えばよいですか? A. 基本はtextContentです。取得が速く、非表示の要素も含めてすべてのテキストを扱えます。innerTextは「画面に見えている文字だけをコピーしたい」「<br> を改行として取り出したい」といった、見た目に合わせたいときに選びます。

Q. innerHTMLは使わないほうがよいのでしょうか? A. 自分で書いた固定のHTMLを入れる用途であれば問題なく使えます。気をつけるのは、入力欄・URL・APIなど外から来た文字列を混ぜる場合です。そのときは値をtextContentで入れる、エスケープ関数に通す、setHTMLやDOMPurifyでサニタイズする、のいずれかを選びます。

Q. setHTMLとDOMPurifyはどちらを選べばよいですか? A. 2026年10月時点ではSafariがsetHTMLに未対応のため、幅広いブラウザーで同じ動きにしたい場合はDOMPurifyが確実です。対応ブラウザーではsetHTMLを使い、それ以外ではDOMPurifyに切り替える書き方(この記事の setSafeHTML)にしておくと、将来ライブラリーなしへ移行しやすくなります。

Q. outerHTMLやinsertAdjacentTextとの関係は? A. outerHTMLは「その要素自身を含むHTML文字列」を取得・置換するプロパティで、文字列を解釈する点はinnerHTMLと同じです。insertAdjacentTextはinsertAdjacentHTMLの文字版で、タグを解釈せず文字として追加します。

まとめ

  • innerHTMLはHTMLとして解釈、textContentは文字としてそのまま、innerTextは画面に見えている文字を扱う。
  • 文字の表示と取得はtextContentが基本。取得は3つの中で最も速く、外から来た文字列も安全に表示できる。
  • innerTextは非表示要素・script・styleを含まず、<br> を改行にする。代入時に改行を <br> に変えるのもinnerTextだけ。
  • innerHTMLに外から来た文字列を入れると、onerrorなどの属性経由でスクリプトが動く。scriptタグが動かないだけでは防げない。
  • 追加は innerHTML += ではなくinsertAdjacentHTMLかappend。既存の要素とリスナーが保たれる。
  • HTMLとして表示したい外部の文字列は、setHTML(Chrome 146・Firefox 148以降)かDOMPurifyでサニタイズしてから入れる。
  • 値を差し込むだけなら、createElementとtextContent、またはエスケープ関数で対応できる。

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「innerHTMLとtextContentの違い|innerText比較表」

株式会社RJCでは、DOM操作と安全なHTMLの書き換え方を仲間と学び合っています。こうした知識を共有しながら、日々技術を磨いています。

株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。

WE ARE HIRING

RJCで一緒に開発しながら、
成長を楽しみませんか?

RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!

  • 127日年間休日
  • 12時間平均残業時間
  • 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!

ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。

ABOUT RJC RJCがどんな会社か知る 考え方、研修、働き方、福利厚生、社員の前職まで。RJCのことが丸わかり! RJC丸わかりページへ JOB DESCRIPTION 仕事内容・待遇を見てみる 仕事内容、給与・待遇、選考の流れ。経験者も未経験も!応募前に知りたいこと、まとめました! 募集要項を見る ENTRY エントリーする エントリーは1〜2分・履歴書不要です。まずは話を聞いてみたい、という方でも歓迎です! エントリーフォームへ

RJCで一緒に開発しながら、 成長を楽しみませんか?