タグ入力フィールド

Enterやカンマでタグを追加し、Backspaceや×で削除できる複数入力UI。重複防止とチップ生成のアニメーション付きで、スキルやキーワード入力に便利です。

#javascript#form#tags

ライブデモ

使用例(お題: アイドルグループ Sakura)

この技法を「アイドルグループ Sakura」というテーマのダミーサイトで実際に使った例です。

HTML
<div class="sk-screen">
  <div class="tg-card">
    <div class="sk-head">
      <span class="sk-logo">🌸 Sakura</span>
      <span class="sk-badge">5th LIVE 応募</span>
    </div>
    <h2 class="tg-title">推しメン&応援キーワード</h2>
    <p class="sk-lead">好きなメンバーや、ライブで叫びたい応援ワードを登録しよう。</p>

    <!-- タグ群+入力。クリックで入力にフォーカス(技法の主役) -->
    <div class="tg-box" id="tg-box">
      <span class="tg-chip">ひまり推し<button type="button" class="tg-x" aria-label="削除">×</button></span>
      <span class="tg-chip">満開ペンライト<button type="button" class="tg-x" aria-label="削除">×</button></span>
      <input id="tg-input" class="tg-input" type="text" placeholder="入力して Enter" autocomplete="off">
    </div>

    <p class="tg-hint">Enterで追加 / Backspaceで末尾を削除 / カンマ区切りも可</p>
  </div>
</div>
CSS
* { box-sizing: border-box; }

body {
  margin: 0;
  min-height: 100vh;
  display: grid;
  place-items: center;
  font-family: "Segoe UI", "Hiragino Kaku Gothic ProN", system-ui, sans-serif;
  background:
    radial-gradient(120% 80% at 50% -10%, #ffd1e0 0%, #ffe8f0 45%, #fbf3f6 100%);
  color: #6a4250;
}

.tg-card {
  width: min(400px, 93vw);
  padding: 22px 24px;
  background: #ffffff;
  border: 1px solid #f6d8e3;
  border-radius: 18px;
  box-shadow: 0 24px 56px -24px rgba(214, 120, 160, 0.55);
}

.sk-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 12px; }
.sk-logo { font-size: 0.95rem; font-weight: 800; color: #e87fa5; letter-spacing: 0.04em; }
.sk-badge {
  font-size: 0.6rem; font-weight: 700; letter-spacing: 0.08em;
  color: #d96a92; padding: 3px 8px;
  background: #ffe3ee; border-radius: 99px;
}

.tg-title { margin: 0 0 4px; font-size: 1.06rem; font-weight: 800; color: #5a3344; }
.sk-lead { margin: 0 0 14px; font-size: 0.74rem; line-height: 1.5; color: #a87f90; }

/* 入力ボックス全体を1つのフィールドに見せる */
.tg-box {
  display: flex; flex-wrap: wrap; gap: 8px;
  padding: 10px; min-height: 52px;
  background: #fff7fb;
  border: 2px solid #f1cdda;
  border-radius: 12px; cursor: text;
  transition: border-color 0.16s ease, box-shadow 0.16s ease, background 0.16s ease;
}
.tg-box.focused { border-color: #ec86ab; background: #fff; box-shadow: 0 0 0 4px rgba(236, 134, 171, 0.18); }

/* タグチップ */
.tg-chip {
  display: inline-flex; align-items: center; gap: 6px;
  padding: 5px 6px 5px 11px;
  font-size: 0.82rem; color: #b13b6a;
  background: #ffe0ec;
  border-radius: 999px;
  animation: chipIn 0.18s ease;
}
.tg-x {
  display: grid; place-items: center;
  width: 18px; height: 18px;
  color: #c14b78; background: rgba(193, 75, 120, 0.16);
  border: none; border-radius: 50%; cursor: pointer;
  font-size: 0.85rem; line-height: 1;
}
.tg-x:hover { background: #d96a92; color: #fff; }

.tg-input {
  flex: 1; min-width: 110px;
  border: none; outline: none; background: transparent;
  font-size: 0.9rem; color: #5a3344;
}
.tg-input::placeholder { color: #d4a8b8; }

.tg-hint { margin: 14px 0 0; font-size: 0.74rem; color: #b58da0; }

@keyframes chipIn {
  from { opacity: 0; transform: scale(0.8); }
  to { opacity: 1; transform: scale(1); }
}

@media (prefers-reduced-motion: reduce) {
  .tg-box { transition: none; }
  .tg-chip { animation: none; }
}
JavaScript
const box = document.getElementById("tg-box");
const input = document.getElementById("tg-input");

// チップ要素を生成
function makeChip(label) {
  const chip = document.createElement("span");
  chip.className = "tg-chip";
  chip.textContent = label; // textContentでXSS防止
  const x = document.createElement("button");
  x.type = "button";
  x.className = "tg-x";
  x.setAttribute("aria-label", "削除");
  x.textContent = "×";
  chip.appendChild(x);
  return chip;
}

// 既存タグ一覧を取得(重複チェック用)
function currentTags() {
  return [...box.querySelectorAll(".tg-chip")].map((c) => c.firstChild.textContent.trim().toLowerCase());
}

// タグを追加(input直前へ挿入)
function addTag(raw) {
  const label = raw.trim();
  if (!label) return;
  if (currentTags().includes(label.toLowerCase())) return; // 重複は無視
  box.insertBefore(makeChip(label), input);
}

if (box && input) {
  // 箱クリックで入力へフォーカス
  box.addEventListener("click", (e) => {
    if (e.target === box) input.focus();
  });

  // ×ボタンで個別削除(イベント委任)
  box.addEventListener("click", (e) => {
    const x = e.target.closest(".tg-x");
    if (x) x.closest(".tg-chip").remove();
  });

  input.addEventListener("focus", () => box.classList.add("focused"));
  input.addEventListener("blur", () => {
    box.classList.remove("focused");
    if (input.value.trim()) { addTag(input.value); input.value = ""; }
  });

  input.addEventListener("keydown", (e) => {
    if (e.key === "Enter" || e.key === ",") {
      e.preventDefault();
      addTag(input.value);
      input.value = "";
    } else if (e.key === "Backspace" && input.value === "") {
      // 空のときBackspaceで末尾チップを削除
      const chips = box.querySelectorAll(".tg-chip");
      if (chips.length) chips[chips.length - 1].remove();
    }
  });

  // カンマ区切りペーストに対応
  input.addEventListener("input", () => {
    if (input.value.includes(",")) {
      input.value.split(",").forEach(addTag);
      input.value = "";
    }
  });
}

コード

HTML
<div class="stage">
  <div class="tg-card">
    <h2 class="tg-title">スキルを追加</h2>

    <!-- タグ群+入力。クリックで入力にフォーカス -->
    <div class="tg-box" id="tg-box">
      <span class="tg-chip">JavaScript<button type="button" class="tg-x" aria-label="削除">×</button></span>
      <span class="tg-chip">CSS<button type="button" class="tg-x" aria-label="削除">×</button></span>
      <input id="tg-input" class="tg-input" type="text" placeholder="入力して Enter" autocomplete="off">
    </div>

    <p class="tg-hint">Enterで追加 / Backspaceで末尾を削除 / カンマ区切りも可</p>
  </div>
</div>
CSS
* { box-sizing: border-box; }

body {
  margin: 0;
  min-height: 100vh;
  display: grid;
  place-items: center;
  font-family: "Segoe UI", "Hiragino Kaku Gothic ProN", system-ui, sans-serif;
  background: linear-gradient(135deg, #134e5e, #71b280);
  color: #1f2937;
}

.stage { width: 100%; padding: 22px; display: grid; place-items: center; }

.tg-card {
  width: min(400px, 93vw);
  padding: 26px 24px;
  background: #fff;
  border-radius: 18px;
  box-shadow: 0 24px 56px -24px rgba(10, 40, 30, 0.5);
}

.tg-title { margin: 0 0 16px; font-size: 1.1rem; color: #1e293b; }

/* 入力ボックス全体を1つのフィールドに見せる */
.tg-box {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  padding: 10px;
  min-height: 52px;
  background: #f1f5f9;
  border: 2px solid #e2e8f0;
  border-radius: 12px;
  cursor: text;
  transition: border-color 0.16s ease, box-shadow 0.16s ease, background 0.16s ease;
}
.tg-box.focused { border-color: #10b981; background: #fff; box-shadow: 0 0 0 4px rgba(16, 185, 129, 0.16); }

/* タグチップ */
.tg-chip {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 5px 6px 5px 11px;
  font-size: 0.82rem;
  color: #065f46;
  background: #d1fae5;
  border-radius: 999px;
  animation: chipIn 0.18s ease;
}
.tg-x {
  display: grid; place-items: center;
  width: 18px; height: 18px;
  color: #047857;
  background: rgba(5, 95, 70, 0.12);
  border: none;
  border-radius: 50%;
  cursor: pointer;
  font-size: 0.85rem;
  line-height: 1;
}
.tg-x:hover { background: #047857; color: #fff; }

.tg-input {
  flex: 1;
  min-width: 110px;
  border: none;
  outline: none;
  background: transparent;
  font-size: 0.9rem;
  color: #1e293b;
}

.tg-hint { margin: 14px 0 0; font-size: 0.76rem; color: #94a3b8; }

@keyframes chipIn {
  from { opacity: 0; transform: scale(0.8); }
  to { opacity: 1; transform: scale(1); }
}

@media (prefers-reduced-motion: reduce) {
  .tg-box { transition: none; }
  .tg-chip { animation: none; }
}
JavaScript
const box = document.getElementById("tg-box");
const input = document.getElementById("tg-input");

// チップ要素を生成
function makeChip(label) {
  const chip = document.createElement("span");
  chip.className = "tg-chip";
  chip.textContent = label; // textContentでXSS防止
  const x = document.createElement("button");
  x.type = "button";
  x.className = "tg-x";
  x.setAttribute("aria-label", "削除");
  x.textContent = "×";
  chip.appendChild(x);
  return chip;
}

// 既存タグ一覧を取得(重複チェック用)
function currentTags() {
  return [...box.querySelectorAll(".tg-chip")].map((c) => c.firstChild.textContent.trim().toLowerCase());
}

// タグを追加(input直前へ挿入)
function addTag(raw) {
  const label = raw.trim();
  if (!label) return;
  if (currentTags().includes(label.toLowerCase())) return; // 重複は無視
  box.insertBefore(makeChip(label), input);
}

if (box && input) {
  // 箱クリックで入力へフォーカス
  box.addEventListener("click", (e) => {
    if (e.target === box) input.focus();
  });

  // ×ボタンで個別削除(イベント委任)
  box.addEventListener("click", (e) => {
    const x = e.target.closest(".tg-x");
    if (x) x.closest(".tg-chip").remove();
  });

  input.addEventListener("focus", () => box.classList.add("focused"));
  input.addEventListener("blur", () => {
    box.classList.remove("focused");
    if (input.value.trim()) { addTag(input.value); input.value = ""; }
  });

  input.addEventListener("keydown", (e) => {
    if (e.key === "Enter" || e.key === ",") {
      e.preventDefault();
      addTag(input.value);
      input.value = "";
    } else if (e.key === "Backspace" && input.value === "") {
      // 空のときBackspaceで末尾チップを削除
      const chips = box.querySelectorAll(".tg-chip");
      if (chips.length) chips[chips.length - 1].remove();
    }
  });

  // カンマ区切りペーストに対応
  input.addEventListener("input", () => {
    if (input.value.includes(",")) {
      input.value.split(",").forEach(addTag);
      input.value = "";
    }
  });
}

🤖 AIエージェント用プロンプト

このままコピーしてAIに貼り付け「追加する場所」だけ書き換えればOK
あなたは熟練のフロントエンドエンジニアです。私のWebサイトに「タグ入力フィールド」の効果を追加してください。

# 追加してほしい効果
タグ入力フィールド(フォーム & 入力)
Enterやカンマでタグを追加し、Backspaceや×で削除できる複数入力UI。重複防止とチップ生成のアニメーション付きで、スキルやキーワード入力に便利です。

# 追加する場所
👉【ここに対象箇所を記入:例「トップのヒーローセクション」「お問い合わせボタン」「記事カードの一覧」など】

# 参考実装(この見た目・挙動を再現してください)
【HTML】
<div class="stage">
  <div class="tg-card">
    <h2 class="tg-title">スキルを追加</h2>

    <!-- タグ群+入力。クリックで入力にフォーカス -->
    <div class="tg-box" id="tg-box">
      <span class="tg-chip">JavaScript<button type="button" class="tg-x" aria-label="削除">×</button></span>
      <span class="tg-chip">CSS<button type="button" class="tg-x" aria-label="削除">×</button></span>
      <input id="tg-input" class="tg-input" type="text" placeholder="入力して Enter" autocomplete="off">
    </div>

    <p class="tg-hint">Enterで追加 / Backspaceで末尾を削除 / カンマ区切りも可</p>
  </div>
</div>

【CSS】
* { box-sizing: border-box; }

body {
  margin: 0;
  min-height: 100vh;
  display: grid;
  place-items: center;
  font-family: "Segoe UI", "Hiragino Kaku Gothic ProN", system-ui, sans-serif;
  background: linear-gradient(135deg, #134e5e, #71b280);
  color: #1f2937;
}

.stage { width: 100%; padding: 22px; display: grid; place-items: center; }

.tg-card {
  width: min(400px, 93vw);
  padding: 26px 24px;
  background: #fff;
  border-radius: 18px;
  box-shadow: 0 24px 56px -24px rgba(10, 40, 30, 0.5);
}

.tg-title { margin: 0 0 16px; font-size: 1.1rem; color: #1e293b; }

/* 入力ボックス全体を1つのフィールドに見せる */
.tg-box {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  padding: 10px;
  min-height: 52px;
  background: #f1f5f9;
  border: 2px solid #e2e8f0;
  border-radius: 12px;
  cursor: text;
  transition: border-color 0.16s ease, box-shadow 0.16s ease, background 0.16s ease;
}
.tg-box.focused { border-color: #10b981; background: #fff; box-shadow: 0 0 0 4px rgba(16, 185, 129, 0.16); }

/* タグチップ */
.tg-chip {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 5px 6px 5px 11px;
  font-size: 0.82rem;
  color: #065f46;
  background: #d1fae5;
  border-radius: 999px;
  animation: chipIn 0.18s ease;
}
.tg-x {
  display: grid; place-items: center;
  width: 18px; height: 18px;
  color: #047857;
  background: rgba(5, 95, 70, 0.12);
  border: none;
  border-radius: 50%;
  cursor: pointer;
  font-size: 0.85rem;
  line-height: 1;
}
.tg-x:hover { background: #047857; color: #fff; }

.tg-input {
  flex: 1;
  min-width: 110px;
  border: none;
  outline: none;
  background: transparent;
  font-size: 0.9rem;
  color: #1e293b;
}

.tg-hint { margin: 14px 0 0; font-size: 0.76rem; color: #94a3b8; }

@keyframes chipIn {
  from { opacity: 0; transform: scale(0.8); }
  to { opacity: 1; transform: scale(1); }
}

@media (prefers-reduced-motion: reduce) {
  .tg-box { transition: none; }
  .tg-chip { animation: none; }
}

【JavaScript】
const box = document.getElementById("tg-box");
const input = document.getElementById("tg-input");

// チップ要素を生成
function makeChip(label) {
  const chip = document.createElement("span");
  chip.className = "tg-chip";
  chip.textContent = label; // textContentでXSS防止
  const x = document.createElement("button");
  x.type = "button";
  x.className = "tg-x";
  x.setAttribute("aria-label", "削除");
  x.textContent = "×";
  chip.appendChild(x);
  return chip;
}

// 既存タグ一覧を取得(重複チェック用)
function currentTags() {
  return [...box.querySelectorAll(".tg-chip")].map((c) => c.firstChild.textContent.trim().toLowerCase());
}

// タグを追加(input直前へ挿入)
function addTag(raw) {
  const label = raw.trim();
  if (!label) return;
  if (currentTags().includes(label.toLowerCase())) return; // 重複は無視
  box.insertBefore(makeChip(label), input);
}

if (box && input) {
  // 箱クリックで入力へフォーカス
  box.addEventListener("click", (e) => {
    if (e.target === box) input.focus();
  });

  // ×ボタンで個別削除(イベント委任)
  box.addEventListener("click", (e) => {
    const x = e.target.closest(".tg-x");
    if (x) x.closest(".tg-chip").remove();
  });

  input.addEventListener("focus", () => box.classList.add("focused"));
  input.addEventListener("blur", () => {
    box.classList.remove("focused");
    if (input.value.trim()) { addTag(input.value); input.value = ""; }
  });

  input.addEventListener("keydown", (e) => {
    if (e.key === "Enter" || e.key === ",") {
      e.preventDefault();
      addTag(input.value);
      input.value = "";
    } else if (e.key === "Backspace" && input.value === "") {
      // 空のときBackspaceで末尾チップを削除
      const chips = box.querySelectorAll(".tg-chip");
      if (chips.length) chips[chips.length - 1].remove();
    }
  });

  // カンマ区切りペーストに対応
  input.addEventListener("input", () => {
    if (input.value.includes(",")) {
      input.value.split(",").forEach(addTag);
      input.value = "";
    }
  });
}

# 外部ライブラリ
なし(追加ライブラリ不要)

# 守ってほしいこと
- 既存のHTML構造・レイアウト・デザインを壊さないこと。必要に応じてクラス名・色・サイズを私のサイトに合わせて調整してよい。
- クラス名やidが既存と衝突しないよう、必要なら接頭辞で名前空間を分けること。
- レスポンシブ対応と prefers-reduced-motion への配慮を入れること。
- 私のサイトのフレームワーク(React / Vue / 素のHTML など)に合わせて実装すること。不明な場合は素のHTML/CSS/JSで提示し、組み込み手順も説明すること。
- 変更後の確認手順も簡潔に教えてください。