JSFiddle - React, Tailwind, and code Playground

HTML

<!DOCTYPE html>
<html>
  <head>
    <title>Parcel Sandbox</title>
    <meta charset="UTF-8" />
  </head>

  <body>
    <button class="add-btn">追加</button>
    <div class="item-box"></div>
    <script src="src/index.js"></script>
  </body>
</html>

JavaScript

document.addEventListener("DOMContentLoaded", function () {
  document.querySelector(".add-btn").addEventListener("click", appendItem);
});
/** 要素を追加する関数 */
function appendItem() {
  /** 追加要素につけるID */
  const randomId = `id-${Math.random()}`;
  /**
   * 入力欄のインデックス。getNextIndex 関数内で空いている最小のインデックスを取得する。
   * これをすると次の様な中抜けの補完ができる。
   * [0, 1, 2] -削除-> [0, 2] -追加-> [0, 1, 2]
   */
  const nextIndex = getNextIndex();
  /**
   *  追加する要素のHTML。ここで先程作ったIDとインデックスを使う。
   *  randomIdを用いることでピンポイントで要素を削除できるようにする。
   *  item[nextIndex][任意キー]とすることで、
   *  サーバー側で受け取ったリクエストでは item[index] 以下の連想配列に意味のある一セットが集まる様にする
   */
  const template = `
    <button type="button" class="btn-remove" onclick="document.getElementById('${randomId}').remove()">削除</button>
    <label>入力欄のラベル</label>
    <div class="input-box">
        <input type="text" name="item[${nextIndex}][name]">
        <input type="text" name="item[${nextIndex}][email]">
    </div>
`;
  // ↑で定義した要素を中身に持つ要素オブジェクトを作る
  const el = document.createElement("div");
  el.id = randomId; // 削除用ランダムID
  el.dataset.index = nextIndex; // この要素がどのインデックスを持つか読み取りやすくする
  el.innerHTML = template; // innerHTML で中身を丸ごと渡す。わりと高速な処理
  // 出来上がった要素を入れるべき場所に追加
  document.querySelector(".item-box").appendChild(el);
}

/** 追加する要素がどのインデックスを取るべきか教える関数 */
function getNextIndex() {
  // 既存要素が使用中のインデックスを列挙
  const usedIndex = Array.from(document.querySelectorAll(".item-box > *")).map(
    (el) => +el.dataset.index
  );
  usedIndex.sort();
  // 抜け番探し
  let prev = usedIndex[0];
  // 0 が存在しないならば、0が抜け番
  let missingNo = prev != null && prev !== 0 ? 0 : null;
  // 順々に見て行ってずれが1出なくなった場合、それが抜け番
  if (missingNo === null) {
    for (let current of usedIndex.slice(1)) {
      if (prev + 1 !== current) {
        missingNo = prev + 1;
        break;
      }
      prev = current;
    }
  }
  // 抜け番が見つかればそれを返す。見つからなければ現在の最大値+1を返す
  return missingNo !== null ? missingNo : usedIndex.slice(-1) + 1;
}