createElementとは、HTMLの要素をJavaScriptで新しく作るメソッドで、document.createElement('li') のようにタグ名を渡して使います。 作っただけではページに表示されず、append や appendChild などで親要素に追加したときに初めて画面に現れます。消すときは remove を呼びます。

この記事では、「作る → 属性・クラス・テキスト・イベントを設定する → 追加する → 削除する」の流れを、そのまま貼って動くコードと実行結果つきでまとめました。追加系メソッド10種類を「挿入位置・引数・戻り値」で比べた早見表、配列からリストやテーブル行を作るコピペ例、文字列挿入でスクリプトが動いてしまう再現と回避、1万件挿入の計測結果まで扱います。掲載した出力は、すべて Chromium 141(Playwright 1.56.0 のヘッドレス実行)で 2026年10月に動かしたものです。Firefox と Safari は未実行です。

createElementと追加・削除メソッドの早見表

まず「どのメソッドが、どこに、何を入れて、何を返すか」を1枚にまとめます。戻り値の列は、下の「戻り値と引数を確かめるコード」で実際に出力した結果です。

メソッド 呼び出す相手 挿入される位置 引数に文字列 複数の引数 戻り値
parent.append(...) 親 親の中の末尾 可(文字として入る) 可 undefined
parent.prepend(...) 親 親の中の先頭 可(文字として入る) 可 undefined
parent.appendChild(node) 親 親の中の末尾 不可(TypeError) 不可(1つだけ) 追加したノード
parent.insertBefore(node, ref) 親 親の中で ref の直前(ref が null なら末尾) 不可 不可 追加したノード
el.before(...) 基準の要素 基準の直前(兄として) 可(文字として入る) 可 undefined
el.after(...) 基準の要素 基準の直後(弟として) 可(文字として入る) 可 undefined
el.insertAdjacentHTML(位置, html) 基準の要素 4か所から指定 必須(HTMLとして解釈) 不可 undefined
el.insertAdjacentElement(位置, 要素) 基準の要素 4か所から指定 不可(要素のみ) 不可 追加した要素
el.replaceWith(...) 置き換えられる要素 自分がいた場所 可(文字として入る) 可 undefined
el.remove() 消す要素 (ページから外れる) 引数なし ― undefined

関連して覚えておくと便利なものも並べます。

やりたいこと 書き方 ひとこと
要素を作る document.createElement('li') 作っただけでは表示されない
中身を全部入れ替える parent.replaceChildren(a, b) 引数なしで空にできる
親から子を消す parent.removeChild(child) 消したノードが返る
複製する el.cloneNode(true) true で子孫ごと複製
まとめて入れる入れ物 document.createDocumentFragment() 追加すると中身だけが移る

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

(記載例)出典:株式会社RJC「JavaScript createElementの使い方|要素の追加・削除」

挿入位置を図で確認する

追加系メソッドは、「親に対して呼ぶもの」と「基準の要素に対して呼ぶもの」の2種類に分かれます。基準の要素を box として、4つの位置を図にすると次のとおりです。

            ← box.before(...)        / insertAdjacent〜('beforebegin', ...)
<div id="box">
            ← box.prepend(...)       / insertAdjacent〜('afterbegin', ...)
  <p>中身</p>
            ← box.append(...)        / insertAdjacent〜('beforeend', ...)
            ← box.appendChild(node)
</div>
            ← box.after(...)         / insertAdjacent〜('afterend', ...)
位置の名前 入る場所 同じ位置に入るメソッド
beforebegin 基準の要素の直前(外側) before
afterbegin 基準の要素の中の先頭 prepend
beforeend 基準の要素の中の末尾 append、appendChild
afterend 基準の要素の直後(外側) after

4つの位置に番号つきの要素を入れて、本当にこの順になるかを確かめます。

<!doctype html>
<html lang="ja">
<head><meta charset="utf-8"><title>挿入位置</title></head>
<body>
  <div id="wrap"><div id="box"><p>中身</p></div></div>
  <script>
    const box = document.querySelector('#box');
    box.insertAdjacentHTML('beforebegin', '<i>1</i>');
    box.insertAdjacentHTML('afterbegin', '<i>2</i>');
    box.insertAdjacentHTML('beforeend', '<i>3</i>');
    box.insertAdjacentHTML('afterend', '<i>4</i>');
    console.log(document.querySelector('#wrap').innerHTML);
  </script>
</body>
</html>

実行結果(コンソール出力):

<i>1</i><div id="box"><i>2</i><p>中身</p><i>3</i></div><i>4</i>

beforebegin と afterend は基準の要素の外側、afterbegin と beforeend は内側に入っています。名前は「開始タグ(begin)の前後」「終了タグ(end)の前後」と読むと覚えやすくなります。

createElementの基本:作る・設定する・追加する・削除する

一連の流れを1ファイルで動かす

次のコードを flow.html という名前で保存し、ブラウザで開いて開発者ツールのコンソールを見ると、同じ出力を確認できます。

<!doctype html>
<html lang="ja">
<head><meta charset="utf-8"><title>createElement の流れ</title></head>
<body>
  <ul id="todo"></ul>
  <script>
    const todo = document.querySelector('#todo');

    // 1. 作る(この時点ではページに表示されない)
    const li = document.createElement('li');
    console.log('追加前 isConnected:', li.isConnected);

    // 2. 属性・クラス・テキストを設定する
    li.id = 'task-1';
    li.className = 'task';
    li.classList.add('is-new');
    li.dataset.id = '1';
    li.setAttribute('title', '牛乳を買う');
    li.textContent = '牛乳を買う';

    // 3. 子要素(削除ボタン)を作り、イベントを付ける
    const btn = document.createElement('button');
    btn.type = 'button';
    btn.textContent = '削除';
    btn.addEventListener('click', () => li.remove());

    // 4. 追加する(li の中にボタン、ul の中に li)
    li.append(' ', btn);
    todo.append(li);
    console.log('追加後 isConnected:', li.isConnected);
    console.log(todo.outerHTML);

    // 5. 削除する(ボタンのクリックで li.remove() が動く)
    btn.click();
    console.log('削除後 isConnected:', li.isConnected);
    console.log(todo.outerHTML);
  </script>
</body>
</html>

実行結果:

追加前 isConnected: false
追加後 isConnected: true
<ul id="todo"><li id="task-1" class="task is-new" data-id="1" title="牛乳を買う">牛乳を買う <button type="button">削除</button></li></ul>
削除後 isConnected: false
<ul id="todo"></ul>

ポイントは3つです。

  • createElement の直後は isConnected が false。ページのどこにも属していない状態で、この間に属性やテキストを設定しておくと、画面の描き直しが1回で済みます。
  • append したときに初めて表示される。li.append(' ', btn) のように、文字列と要素を並べて渡せます。
  • remove するとページから外れる。変数 li が残っていれば、もう一度 append して戻すこともできます。

属性・クラス・テキスト・イベントの設定方法一覧

設定したいもの 書き方 補足
id el.id = 'task-1' プロパティに代入
クラス(まとめて) el.className = 'task done' 既存のクラスは上書き
クラス(追加・削除・切替) el.classList.add('a') / remove('a') / toggle('a') 既存のクラスを保ったまま操作
テキスト el.textContent = '文字' タグは文字として表示される
任意の属性 el.setAttribute('aria-label', '閉じる') aria-* など何でも設定可
data属性 el.dataset.userId = '7' HTML上は data-user-id="7"
リンク先・画像 a.href = '/list'、img.src = 'a.png'、img.alt = '説明' プロパティに代入
フォーム部品 input.type = 'text'、input.value = '初期値'、input.disabled = true 真偽値はそのまま代入
スタイル el.style.color = 'red'、el.style.setProperty('--gap', '8px') 見た目はクラスの付け外しで切り替える方が管理しやすい
イベント el.addEventListener('click', handler) 同じ要素に複数付けられる

プロパティ代入と setAttribute はどちらでも設定できる属性が多いのですが、class は className、for は htmlFor とプロパティ名が変わる点だけ覚えておくと迷いません。独自の属性や aria-* は setAttribute を使うと確実です。

タグ名の書き方とSVG

createElement の引数はタグ名の文字列です。大文字で渡しても小文字の要素として作られ、tagName は大文字で返ります。SVG の要素は createElementNS で名前空間を指定して作ります。実行して確かめた結果は「エラー文と対処の一覧」に載せています。

戻り値と引数を確かめるコード

早見表の内容を、1つずつ実行して確かめます。

<!doctype html>
<html lang="ja">
<head><meta charset="utf-8"><title>戻り値と引数</title></head>
<body>
  <ul id="list"><li id="a">A</li><li id="b">B</li></ul>
  <script>
    const list = document.querySelector('#list');
    const a = document.querySelector('#a');
    const b = document.querySelector('#b');
    const li = (text) => {
      const el = document.createElement('li');
      el.textContent = text;
      return el;
    };

    const x = li('x');
    console.log('appendChild          :', list.appendChild(x) === x);
    console.log('append               :', list.append(li('y'), '文字', li('z')));
    console.log('prepend              :', list.prepend(li('先頭')));
    console.log('before               :', a.before(li('Aの前')));
    console.log('after                :', a.after(li('Aの後')));
    const n = li('Bの前');
    console.log('insertBefore         :', list.insertBefore(n, b) === n);
    console.log('insertAdjacentHTML   :', b.insertAdjacentHTML('afterend', '<li>HTML文字列</li>'));
    const e = li('要素');
    console.log('insertAdjacentElement:', b.insertAdjacentElement('beforebegin', e) === e);
    console.log('replaceWith          :', a.replaceWith(li('Aの代わり')));
    console.log('remove               :', x.remove());
    console.log([...list.childNodes].map((c) => c.textContent).join(' / '));

    // 文字列を渡したときの違い
    const box = document.createElement('div');
    box.append('<b>太字?</b>');
    console.log('append に文字列      :', box.innerHTML, '要素数', box.children.length);
    try {
      box.appendChild('<b>太字?</b>');
    } catch (err) {
      console.log('appendChild に文字列 :', err.name, err.message);
    }
  </script>
</body>
</html>

実行結果:

appendChild          : true
append               : undefined
prepend              : undefined
before               : undefined
after                : undefined
insertBefore         : true
insertAdjacentHTML   : undefined
insertAdjacentElement: true
replaceWith          : undefined
remove               : undefined
先頭 / Aの前 / Aの代わり / Aの後 / Bの前 / 要素 / B / HTML文字列 / y / 文字 / z
append に文字列      : &lt;b&gt;太字?&lt;/b&gt; 要素数 0
appendChild に文字列 : TypeError Failed to execute 'appendChild' on 'Node': parameter 1 is not of type 'Node'.

ここから読み取れることを整理します。

  • 追加したノードを返すのは appendChild・insertBefore・insertAdjacentElement の3つ。const li = ul.appendChild(document.createElement('li')) のように、作成と追加を1行で書けます。
  • append・prepend・before・after・replaceWith は undefined を返すので、戻り値を変数に受けて使う書き方はできません。先に変数へ入れてから渡します。
  • append に渡した文字列はタグとして解釈されず、文字のまま入る(&lt;b&gt; と表示用に置き換えられ、子要素は0個)。HTMLとして解釈させたいときだけ insertAdjacentHTML を使います。
  • appendChild に文字列を渡すと TypeError で止まります。

appendとappendChildの違い

比べる点 append appendChild
渡せるもの ノードと文字列 ノードのみ
一度に渡せる数 いくつでも 1つ
戻り値 undefined 追加したノード
引数なしで呼ぶ 何も起きない TypeError
向いている場面 複数まとめて追加、文字と要素を混ぜる 追加した要素をそのまま変数で受けたい

新しく書くコードは append を基本にして、戻り値が欲しいときだけ appendChild という使い分けで困ることはほぼありません。先頭に入れるなら prepend、特定の要素の前後なら before / after が素直に書けます。

insertAdjacentHTML・insertAdjacentElement・insertAdjacentTextの違い

メソッド 第2引数 解釈のされ方 戻り値
insertAdjacentHTML HTML文字列 タグとして解釈して要素を作る undefined
insertAdjacentElement 要素 その要素をそのまま入れる 入れた要素
insertAdjacentText 文字列 文字として入れる undefined

insertAdjacentHTML は、固定のHTMLをまとめて入れたいときに短く書けるのが魅力です。一方で、渡した文字列がHTMLとして解釈されるため、利用者が入力した文字を混ぜる場合は次の章の注意が必要です。

配列からリストやテーブル行を作るコピペ例

一覧表示でよく使う「配列 → li」「オブジェクトの配列 → tr」「削除ボタン」「全件の入れ替え」を1ファイルにまとめました。

<!doctype html>
<html lang="ja">
<head><meta charset="utf-8"><title>配列からリストと表を作る</title></head>
<body>
  <ul id="menu"></ul>
  <table>
    <thead><tr><th>ID</th><th>品名</th><th>在庫</th><th></th></tr></thead>
    <tbody id="rows"></tbody>
  </table>
  <script>
    // --- 配列 → li ---
    const fruits = ['りんご', 'みかん', '<b>ぶどう</b>'];
    const menu = document.querySelector('#menu');
    const lis = fruits.map((name) => {
      const li = document.createElement('li');
      li.textContent = name; // 文字として入るのでタグは解釈されない
      return li;
    });
    menu.append(...lis); // 複数まとめて追加
    console.log(menu.outerHTML);

    // --- オブジェクトの配列 → tr ---
    const items = [
      { id: 1, name: 'ノートPC', stock: 12 },
      { id: 2, name: 'モニター', stock: 5 },
      { id: 3, name: 'キーボード', stock: 0 },
    ];
    const tbody = document.querySelector('#rows');
    for (const item of items) {
      const tr = tbody.insertRow();        // tr を作って末尾に追加
      tr.dataset.id = item.id;
      tr.insertCell().textContent = item.id;
      tr.insertCell().textContent = item.name;
      tr.insertCell().textContent = item.stock;
      const del = document.createElement('button');
      del.type = 'button';
      del.textContent = '削除';
      tr.insertCell().append(del);
    }
    console.log('行数:', tbody.rows.length);
    console.log(tbody.rows[0].outerHTML);

    // --- 削除ボタンは親でまとめて受ける(イベント委譲) ---
    tbody.addEventListener('click', (event) => {
      const button = event.target.closest('button');
      if (!button) return;
      button.closest('tr').remove();
    });
    tbody.rows[1].querySelector('button').click();
    console.log('削除後の行数:', tbody.rows.length,
      [...tbody.rows].map((tr) => tr.dataset.id).join(','));

    // --- 全部作り直す ---
    menu.replaceChildren(...['バナナ', 'もも'].map((name) => {
      const li = document.createElement('li');
      li.textContent = name;
      return li;
    }));
    console.log(menu.outerHTML);
    menu.replaceChildren(); // 引数なしで空にする
    console.log(menu.outerHTML);
  </script>
</body>
</html>

実行結果:

<ul id="menu"><li>りんご</li><li>みかん</li><li>&lt;b&gt;ぶどう&lt;/b&gt;</li></ul>
行数: 3
<tr data-id="1"><td>1</td><td>ノートPC</td><td>12</td><td><button type="button">削除</button></td></tr>
削除後の行数: 2 1,3
<ul id="menu"><li>バナナ</li><li>もも</li></ul>
<ul id="menu"></ul>

このコードの要点は次のとおりです。

  • map で要素の配列を作り、append(...配列) でまとめて追加すると、ループの中で1件ずつ追加するより短く書けます。
  • テーブルは insertRow() と insertCell() が便利です。tr と td を作って追加するところまでを1回の呼び出しで済ませ、作った行・セルを返してくれます。
  • textContent で入れているので、データに <b> のような文字が含まれていても文字のまま表示されます(3件目の「ぶどう」)。
  • 削除ボタンのイベントは、行ごとではなく tbody に1つだけ付ける(イベント委譲)と、あとから追加した行でもそのまま動きます。closest('tr') で押されたボタンの行をたどれます。
  • replaceChildren(...新しい要素) は「全部消して入れ直す」を1回で行うメソッドです。検索結果の再表示などに向いています。

子要素が多い部品を何度も作る場合は、HTML側に template 要素でひな形を書き、tpl.content.cloneNode(true) で複製して textContent で中身を埋める方法もあります(複製結果は、あとの章で説明する DocumentFragment です)。

文字列挿入でスクリプトが動く例とtextContentでの回避

insertAdjacentHTML や innerHTML は渡した文字列をHTMLとして解釈します。利用者が入力した文字列をそのまま渡すと、入力に含まれたスクリプトがページ内で動くことがあります(クロスサイトスクリプティング、略して XSS と呼ばれる問題です)。手元で再現してみます。

<!doctype html>
<html lang="ja">
<head><meta charset="utf-8"><title>文字列挿入とスクリプト混入</title></head>
<body>
  <div id="c1"></div><div id="c2"></div><div id="c3"></div><div id="c4"></div><div id="c5"></div>
  <script>
    const hits = [];
    window.mark = (label) => hits.push(label);

    // 利用者が入力欄に入れた文字列、という想定
    const input1 = '<img src="x" onerror="mark(\'img onerror\')">';
    const input2 = '<script>mark("script タグ")<\/script>';

    // HTML として解釈する挿入
    document.querySelector('#c1').insertAdjacentHTML('beforeend', input1);
    document.querySelector('#c2').innerHTML = input2;
    document.querySelector('#c2').insertAdjacentHTML('beforeend', input2);

    // 文字として入れる挿入
    document.querySelector('#c3').textContent = input1;
    document.querySelector('#c4').append(input1);
    document.querySelector('#c5').insertAdjacentText('beforeend', input1);

    setTimeout(() => {
      console.log('動いたスクリプト:', JSON.stringify(hits));
      console.log('c1 の img 要素数:', document.querySelectorAll('#c1 img').length);
      console.log('c3〜c5 の img 要素数:', document.querySelectorAll('#c3 img, #c4 img, #c5 img').length);
      console.log('c3 の中身:', document.querySelector('#c3').innerHTML);
    }, 500);
  </script>
</body>
</html>

実行結果:

動いたスクリプト: ["img onerror"]
c1 の img 要素数: 1
c3〜c5 の img 要素数: 0
c3 の中身: &lt;img src="x" onerror="mark('img onerror')"&gt;
入れ方 渡した文字列 結果
insertAdjacentHTML img タグ+ onerror 属性 スクリプトが動いた
innerHTML / insertAdjacentHTML script タグ 動かなかった
textContent img タグ+ onerror 属性 動かない(文字として表示)
append(文字列) 同上 動かない(文字として表示)
insertAdjacentText 同上 動かない(文字として表示)

script タグは文字列挿入では実行されませんが、img の onerror のようなイベント属性は実行されます。 「script タグを取り除けば十分」とは言えない、というのがこの再現でわかる点です。

対策の方針は単純で、利用者の入力や外部から受け取った値は textContent(または append に文字列で渡す)で入れることです。HTMLの骨組みが必要なら、骨組みだけを createElement や template で作り、値の部分を textContent で埋めます。

// 向かない書き方:値を HTML 文字列に連結している
list.insertAdjacentHTML('beforeend', '<li>' + userName + '</li>');

// おすすめ:要素を作り、値は textContent で入れる
const li = document.createElement('li');
li.textContent = userName;
list.append(li);
入れる内容 おすすめの方法
利用者の入力、APIの応答など外から来た値 createElement + textContent
自分で書いた固定のHTML(値を混ぜない) insertAdjacentHTML でも可
固定の骨組み+外から来た値 template を複製して textContent で埋める
どうしても外から来たHTMLを表示したい 無害化(サニタイズ)用のライブラリを通してから挿入

なお、サニタイズ用ライブラリの動作は今回は未実行です。

DocumentFragmentと一括挿入の計測結果

DocumentFragmentとは

DocumentFragment は、ページに属さない「仮の入れ物」です。要素をためておいて親に追加すると、入れ物自体は入らず、中身だけが移動します。

frag.append(li1, li2) のあと ul.append(frag) を実行して frag.childNodes.length を調べると、追加前は 2、追加後は 0 でした(Chromium 141)。

1万件を挿入して比べる

「ループの中で1件ずつ追加すると遅いので DocumentFragment を使う」と説明されることがあります。実際にどのくらい差があるのか、li を1万件追加して測りました。DOM操作そのものの時間と、その直後のレイアウト計算(位置や大きさの計算)の時間を分けています。

<!doctype html>
<html lang="ja">
<head><meta charset="utf-8"><title>一括挿入の計測</title></head>
<body>
  <div id="app"></div>
  <script>
    const app = document.querySelector('#app');
    const N = 10000;
    const li = (i) => {
      const el = document.createElement('li');
      el.textContent = 'item ' + i;
      return el;
    };
    const html = (n) => {
      let s = '';
      for (let i = 0; i < n; i++) s += '<li>item ' + i + '</li>';
      return s;
    };

    // 9回測って中央値を出す。DOM 操作の時間と、その後のレイアウト計算の時間を分ける
    function bench(name, fn) {
      const dom = [];
      const layout = [];
      for (let run = 0; run < 9; run++) {
        app.innerHTML = '<ul id="ul"></ul>';
        const ul = document.querySelector('#ul');
        void ul.offsetHeight;
        const t0 = performance.now();
        fn(ul);
        const t1 = performance.now();
        void ul.offsetHeight; // ここでレイアウト計算が走る
        const t2 = performance.now();
        dom.push(t1 - t0);
        layout.push(t2 - t1);
      }
      const median = (a) => a.sort((x, y) => x - y)[4].toFixed(1);
      console.log(name.padEnd(34), 'DOM', median(dom).padStart(6), 'ms / レイアウト', median(layout).padStart(6), 'ms');
    }

    bench('appendChild を1万回', (ul) => {
      for (let i = 0; i < N; i++) ul.appendChild(li(i));
    });
    bench('DocumentFragment にためて1回', (ul) => {
      const frag = document.createDocumentFragment();
      for (let i = 0; i < N; i++) frag.appendChild(li(i));
      ul.appendChild(frag);
    });
    bench('append(...配列) で1回', (ul) => {
      const nodes = [];
      for (let i = 0; i < N; i++) nodes.push(li(i));
      ul.append(...nodes);
    });
    bench('innerHTML に1回代入', (ul) => {
      ul.innerHTML = html(N);
    });
    bench('insertAdjacentHTML を1回', (ul) => {
      ul.insertAdjacentHTML('beforeend', html(N));
    });
    bench('appendChild を1000回', (ul) => {
      for (let i = 0; i < 1000; i++) ul.appendChild(li(i));
    });
    bench('innerHTML += を1000回', (ul) => {
      for (let i = 0; i < 1000; i++) ul.innerHTML += '<li>item ' + i + '</li>';
    });
    bench('appendChild+offsetHeight を1000回', (ul) => {
      for (let i = 0; i < 1000; i++) {
        ul.appendChild(li(i));
        void ul.offsetHeight; // 追加のたびに高さを読む
      }
    });
  </script>
</body>
</html>

実行結果(Chromium 141 ヘッドレス、各9回の中央値。数値は実行環境で変わります):

方法 件数 DOM操作 レイアウト 読み取れること
appendChild を繰り返す 1万 6.0 ms 149.4 ms 1件ずつでもDOM操作は数ミリ秒
DocumentFragment で1回 1万 5.3 ms 127.8 ms DOM操作の時間はほぼ同じ
append(...配列) で1回 1万 5.1 ms 125.1 ms 同上
innerHTML に1回代入 1万 2.7 ms 120.0 ms 文字列の組み立てを含めても最短
insertAdjacentHTML を1回 1万 2.6 ms 120.7 ms 同上
appendChild を繰り返す 1000 0.4 ms 11.6 ms 比較用
innerHTML += を繰り返す 1000 215.1 ms 14.2 ms 件数が10分の1でも桁違いに長い
追加のたびに offsetHeight を読む 1000 145.7 ms 0.0 ms 毎回レイアウト計算が走る

この計測から言えることは次の3点です。

  • この環境では、ループ内の appendChild と DocumentFragment の差はDOM操作で1ミリ秒未満でした。ブラウザは追加のたびに画面を描き直すのではなく、スクリプトが一区切りついたところでまとめてレイアウトを計算するためです。時間の大半は、どの方法でも発生するレイアウト計算でした。
  • 時間が大きく伸びるのは innerHTML += をループで繰り返す書き方です。+= は「今の中身を文字列に戻す → 連結する → 全体を解釈し直す」を毎回行うので、件数が増えるほど1回あたりの処理も増えます。しかも、中にあった要素は作り直されるため、付けておいたイベントリスナーや入力途中の値も引き継がれません。
  • 追加の合間に offsetHeight などレイアウトに関わる値を読むと、そのたびに計算が走ります。1000件で 145.7 ms と、読まない場合(0.4 ms + 11.6 ms)の10倍以上でした。高さや位置の読み取りは、追加をすべて終えてから行うのが効率的です。

DocumentFragment は速度のためというより、「複数の要素をひとまとまりとして受け渡す」「template の複製結果を扱う」ための道具と捉えると使いどころがはっきりします。関数が複数の要素を返したいとき、配列の代わりに DocumentFragment を返せば、受け取った側は append 1回で済みます。

既存の要素をappendすると「移動」になる

すでにページにある要素を別の場所に append すると、複製されるのではなく、元の場所から新しい場所へ移動します。1つのノードが同時に2か所に存在することはない、という決まりによるものです。

<!doctype html>
<html lang="ja">
<head><meta charset="utf-8"><title>append は移動になる</title></head>
<body>
  <ul id="left"><li id="item">りんご</li><li>みかん</li></ul>
  <ul id="right"></ul>
  <script>
    const left = document.querySelector('#left');
    const right = document.querySelector('#right');
    const item = document.querySelector('#item');

    right.append(item); // すでにページにある要素を append
    console.log('left :', left.innerHTML);
    console.log('right:', right.innerHTML);

    // 両方に置きたいときは複製する
    const copy = item.cloneNode(true);
    copy.removeAttribute('id'); // id の重複を避ける
    left.append(copy);
    console.log('left :', left.innerHTML);

    // cloneNode はイベントリスナーを複製しない
    let count = 0;
    const btn = document.createElement('button');
    btn.addEventListener('click', () => count++);
    const btnCopy = btn.cloneNode(true);
    btn.click();
    btnCopy.click();
    console.log('クリック2回で count =', count);

    // 同じ要素を2回 append しても増えない
    const li = document.createElement('li');
    li.textContent = 'もも';
    right.append(li);
    right.append(li);
    console.log('right の li 数:', right.children.length);
  </script>
</body>
</html>

実行結果:

left : <li>みかん</li>
right: <li id="item">りんご</li>
left : <li>みかん</li><li>りんご</li>
クリック2回で count = 1
right の li 数: 2
挙動 結果 対応
ページ内の要素を別の親に append 元の場所から消えて移動する 並べ替えや「移動」はこの性質をそのまま使える
両方に表示したい そのままでは片方にしか出ない cloneNode(true) で複製してから追加
複製した要素の id 元と同じ id が付く removeAttribute('id') などで重複を避ける
複製した要素のイベント addEventListener で付けたものは複製されない(クリック2回で count は 1) 複製後に付け直すか、親でまとめて受ける
同じ要素を2回 append 増えない(末尾へ移動するだけ) 件数分だけ createElement する

ループの外で1回だけ createElement して、ループの中で何度も append すると、最後の1つしか残りません。 リストが1件しか表示されないときは、createElement がループの中にあるかを確認してみてください。

要素を削除する方法の使い分け

<!doctype html>
<html lang="ja">
<head><meta charset="utf-8"><title>削除のパターン</title></head>
<body>
  <ul id="list"><li class="done">A</li><li>B</li><li class="done">C</li><li class="done">D</li><li>E</li></ul>
  <script>
    const list = document.querySelector('#list');
    const text = () => [...list.children].map((li) => li.textContent).join('');
    const reset = () => {
      list.innerHTML = '<li class="done">A</li><li>B</li><li class="done">C</li><li class="done">D</li><li>E</li>';
    };

    // 1. 自分自身を消す
    list.firstElementChild.remove();
    console.log('remove            :', text());

    // 2. 親から子を消す(消した要素が返る)
    const removed = list.removeChild(list.lastElementChild);
    console.log('removeChild       :', text(), '/ 戻り値', removed.textContent);

    // 3. 条件に合うものだけ消す(querySelectorAll は静的な一覧)
    reset();
    list.querySelectorAll('.done').forEach((li) => li.remove());
    console.log('.done を削除      :', text());

    // 4. children は生きた一覧。前から消すと番号がずれる
    reset();
    for (let i = 0; i < list.children.length; i++) {
      list.children[i].remove();
    }
    console.log('前から for で削除 :', text());

    // 5. 全部消す
    reset();
    list.replaceChildren();
    console.log('replaceChildren   :', JSON.stringify(list.innerHTML));

    // 6. 存在しない要素に remove を呼ぶと TypeError
    try {
      document.querySelector('#nothing').remove();
    } catch (err) {
      console.log(err.name + ': ' + err.message);
    }
    document.querySelector('#nothing')?.remove(); // ?. なら何も起きない
    console.log('?. は例外なし');
  </script>
</body>
</html>

実行結果:

remove            : BCDE
removeChild       : BCD / 戻り値 E
.done を削除      : BE
前から for で削除 : BD
replaceChildren   : ""
TypeError: Cannot read properties of null (reading 'remove')
?. は例外なし
やりたいこと 書き方 補足
その要素を消す el.remove() 親を取得しなくてよい
親から子を消して、消したものを受け取る parent.removeChild(child) 戻り値は消したノード
条件に合う要素をまとめて消す parent.querySelectorAll('.done').forEach((el) => el.remove()) querySelectorAll の結果は静的なので安心して回せる
中身を空にする parent.replaceChildren() parent.textContent = '' でも空になる
中身を入れ替える parent.replaceChildren(a, b) 削除と追加を1回で
別の要素に置き換える el.replaceWith(newEl) 自分の位置に新しい要素が入る

4番目の出力に注目してください。children は「生きた一覧」で、要素を消すとその場で番号が詰まります。 前から順に children[i].remove() すると1つ飛ばしになり、5件中2件(B と D)が残りました。まとめて消すときは querySelectorAll の結果を回すか、replaceChildren() を使うと意図どおりになります。

また、querySelector は見つからないと null を返すため、そのまま .remove() を呼ぶと TypeError になります。「あれば消す」は document.querySelector('#x')?.remove() と書くと1行で済みます。

エラー文と対処の一覧

追加・削除でよく出会うエラーを、実際に発生させて文面を集めました(「発生させたコード」を1行ずつ実行した結果です)。

発生させたコード 出力されたエラー 対処
head 内のスクリプトで document.querySelector('#app').append('x') TypeError: Cannot read properties of null (reading 'append') body の読み込み前で null が返っている。スクリプトを body の末尾に置く、DOMContentLoaded で待つ、script に defer を付ける
document.createElement('my tag') InvalidCharacterError: Failed to execute 'createElement' on 'Document': The tag name provided ('my tag') is not a valid name. タグ名だけを渡す。クラスや属性は作成後に設定
app.insertAdjacentElement('inside', p) SyntaxError: Failed to execute 'insertAdjacentElement' on 'Element': The value provided ('inside') is not one of 'beforeBegin', 'afterBegin', 'beforeEnd', or 'afterEnd'. 位置は4種類から選ぶ
app.appendChild('文字列') TypeError: Failed to execute 'appendChild' on 'Node': parameter 1 is not of type 'Node'. append を使うか、ノードを渡す
app.insertBefore(p, document.body) NotFoundError: Failed to execute 'insertBefore' on 'Node': The node before which the new node is to be inserted is not a child of this node. 第2引数はその親の子にする。ref.before(p) の形なら親を意識しなくてよい
app.append(document.body) HierarchyRequestError: Failed to execute 'append' on 'Element': The new child element contains the parent. 親や先祖を自分の中には入れられない。追加する向きを確認

エラーにはならないものの、戸惑いやすい挙動も2つ確かめました。

コード 結果 補足
document.createElement('circle').constructor.name HTMLUnknownElement SVG の要素にならない。document.createElementNS('http://www.w3.org/2000/svg', 'circle') なら SVGCircleElement
document.createElement('div').tagName DIV 大文字で返る。createElement('DIV') と大文字で渡しても、できる要素は div

エラー文は Chromium のものです。他のブラウザでは文面が異なります(未実行)。

どのメソッドを選ぶかの判断表

状況 選ぶもの 理由
親の末尾に1つ以上追加したい append 複数・文字列も渡せる
親の先頭に追加したい prepend insertBefore(node, parent.firstChild) より短い
ある要素の前後に追加したい before / after 親を取得しなくてよい
追加した要素をそのまま変数に受けたい appendChild 追加したノードを返す
固定のHTMLをまとめて入れたい insertAdjacentHTML 既存の中身を保ったまま追記できる
外から来た値を表示したい createElement + textContent 値がHTMLとして解釈されない
同じ形の部品をたくさん作りたい template + cloneNode(true) 骨組みをHTMLで書ける
一覧を作り直したい replaceChildren(...要素) 削除と追加が1回
要素を別の要素に差し替えたい replaceWith 位置を保ったまま交換
要素を消したい remove 自分自身に対して呼ぶだけ
既存の中身に追記したい append か insertAdjacentHTML innerHTML += は中身全体を作り直すため、追記には向かない

よくある質問

Q. createElementで作った要素が画面に表示されません。 A. createElement は要素を作るだけで、ページには追加しません。parent.append(el) などで親に追加すると表示されます。追加したのに表示されない場合は、追加先の querySelector が null になっていないか(スクリプトが body の読み込み前に動いていないか)を確認してください。

Q. appendとappendChildはどちらを使えばよいですか。 A. 迷ったら append で問題ありません。複数の要素や文字列を一度に渡せます。追加した要素を戻り値で受け取りたいときだけ appendChild を使います。append の戻り値は undefined です。

Q. insertAdjacentHTMLとinnerHTMLはどう違いますか。 A. innerHTML への代入は中身を丸ごと置き換え、insertAdjacentHTML は既存の中身を残したまま指定した位置に追加します。どちらも文字列をHTMLとして解釈するので、外から来た値を混ぜる場合は textContent を使う形に切り替えるのがおすすめです。

Q. ループで追加したのに1件しか表示されません。 A. createElement をループの外で1回だけ呼んでいる可能性があります。同じ要素を何度 append しても増えず、移動するだけです。createElement をループの中に入れて、件数分の要素を作ってください。

まとめ

  • createElement は作るだけ。append などで親に追加して初めて表示される(追加前の isConnected は false)。
  • 追加は append / prepend / before / after の4つで位置を選ぶ。戻り値が欲しいときは appendChild / insertBefore / insertAdjacentElement。
  • append に渡した文字列は文字として入る。HTMLとして解釈するのは insertAdjacentHTML と innerHTML。
  • 外から来た値は textContent で入れる。insertAdjacentHTML に渡すと、img の onerror のような属性でスクリプトが動くことを再現で確認した。
  • 既存の要素を append すると移動になる。複製は cloneNode(true)、イベントリスナーは複製されない。
  • 削除は remove、全消去と入れ替えは replaceChildren。children を前から消すと1つ飛ばしになる。
  • 1万件の計測では、ループ内 appendChild と DocumentFragment の差はごくわずか。時間が伸びるのは innerHTML += の繰り返しと、追加の合間のレイアウト値の読み取り。

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

(記載例)出典:株式会社RJC「JavaScript createElementの使い方|要素の追加・削除」

株式会社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で一緒に開発しながら、 成長を楽しみませんか?