タグ入力
入力欄にEnterでタグを追加し、×ボタンで削除できるチップ式のタグ入力。大文字小文字を無視して重複を検知し、既存タグを軽く揺らして知らせます。
ライブデモ
コード
HTML
<!-- タグ入力: Enterでチップ追加、×で削除、大小無視で重複はシェイクして拒否 -->
<section class="panel" aria-label="タグ入力のデモ">
<label class="field-label" for="tagInput">興味のあるタグ</label>
<div class="tag-field" id="tagField">
<!-- チップはJSがinputの直前に動的挿入する -->
<input
type="text"
id="tagInput"
class="tag-input"
placeholder="タグを入力してEnter"
autocomplete="off"
aria-describedby="tagCount"
>
</div>
<p class="field-hint" id="tagCount" role="status" aria-live="polite">0 個のタグ</p>
</section>
CSS
* { box-sizing: border-box; }
body {
margin: 0;
min-height: 100vh;
display: grid;
place-items: center;
padding: 24px;
font-family: "Segoe UI", system-ui, sans-serif;
background: linear-gradient(160deg, #fdfbf6 0%, #f5efe3 55%, #ece2d0 100%);
}
.panel {
width: min(380px, 92vw);
background: #ffffff;
border-radius: 20px;
padding: 26px 26px 22px;
box-shadow: 0 24px 48px -22px rgba(120, 92, 40, .22), 0 2px 8px rgba(60, 45, 20, .06);
border: 1px solid rgba(180, 150, 100, .18);
}
.field-label {
display: block;
margin: 0 0 12px;
font-size: 13px;
font-weight: 700;
color: #57534e;
letter-spacing: .02em;
}
.tag-field {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 8px;
padding: 10px 12px;
border-radius: 14px;
background: #faf7f0;
border: 1.5px solid #e7ddc8;
transition: border-color .2s ease, box-shadow .2s ease;
}
.tag-field:focus-within {
border-color: #d6a94c;
box-shadow: 0 0 0 3px rgba(214, 169, 76, .18);
}
/* チップ(.chip)はtag-field直下にJSで挿入され、inputと同じ行でフローする */
.chip {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 6px 8px 6px 12px;
border-radius: 999px;
background: #fdf0d8;
color: #92660f;
font-size: 13px;
font-weight: 600;
animation: chip-in .28s cubic-bezier(.34, 1.56, .64, 1);
}
.chip__label { line-height: 1; }
.chip__remove {
appearance: none;
border: none;
background: rgba(146, 102, 15, .12);
color: #92660f;
width: 18px;
height: 18px;
border-radius: 50%;
font-size: 12px;
line-height: 1;
cursor: pointer;
display: grid;
place-items: center;
transition: background .15s ease, transform .15s ease;
}
.chip__remove:hover { background: rgba(146, 102, 15, .24); }
.chip__remove:active { transform: scale(.88); }
.chip__remove:focus-visible { outline: 2px solid #d6a94c; outline-offset: 2px; }
/* 重複入力時の拒否フィードバック */
.chip.is-dup { animation: chip-shake .38s ease; }
.tag-input {
flex: 1 1 120px;
min-width: 100px;
border: none;
background: transparent;
font-size: 14px;
color: #3d3a34;
padding: 4px 2px;
}
.tag-input:focus { outline: none; }
.tag-input::placeholder { color: #b7ac96; }
.field-hint {
margin: 12px 2px 0;
font-size: 12px;
color: #a89a78;
}
@keyframes chip-in {
0% { transform: scale(.6); opacity: 0; }
100% { transform: scale(1); opacity: 1; }
}
@keyframes chip-shake {
0%, 100% { transform: translateX(0); }
20% { transform: translateX(-5px); }
40% { transform: translateX(4px); }
60% { transform: translateX(-3px); }
80% { transform: translateX(2px); }
}
@media (prefers-reduced-motion: reduce) {
.chip, .chip.is-dup { animation: none !important; }
}
JavaScript
// タグ入力: Enterでチップ追加、×ボタンで削除。大小無視で重複ならシェイクして拒否する。
(() => {
const field = document.getElementById('tagField');
const input = document.getElementById('tagInput');
const count = document.getElementById('tagCount');
if (!field || !input || !count) return; // null安全
let tags = ['デザイン', 'リサーチ']; // 初期表示用のサンプルタグ
const render = () => {
field.querySelectorAll('.chip').forEach((el) => el.remove());
tags.forEach((tag) => {
const chip = document.createElement('span');
chip.className = 'chip';
const label = document.createElement('span');
label.className = 'chip__label';
label.textContent = tag; // textContentでXSSを回避
const remove = document.createElement('button');
remove.type = 'button';
remove.className = 'chip__remove';
remove.setAttribute('aria-label', `${tag} を削除`);
remove.textContent = '×';
remove.addEventListener('click', () => {
tags = tags.filter((t) => t !== tag);
render();
input.focus();
});
chip.append(label, remove);
field.insertBefore(chip, input);
});
count.textContent = `${tags.length} 個のタグ`;
};
const shake = (index) => {
const chips = field.querySelectorAll('.chip');
const target = chips[index];
if (!target) return;
target.classList.remove('is-dup');
void target.offsetWidth; // リフローでアニメを再生成
target.classList.add('is-dup');
};
input.addEventListener('keydown', (e) => {
if (e.key !== 'Enter') return;
e.preventDefault();
const raw = input.value.trim();
if (!raw) return;
const dupIndex = tags.findIndex((t) => t.toLowerCase() === raw.toLowerCase());
if (dupIndex !== -1) {
shake(dupIndex); // 既存タグと重複 -> 追加せず既存チップを揺らして知らせる
input.value = '';
return;
}
tags.push(raw);
render();
input.value = '';
});
render();
})();
🤖 AIエージェント用プロンプト
このままコピーしてAIに貼り付け「追加する場所」だけ書き換えればOK
あなたは熟練のフロントエンドエンジニアです。私のWebサイトに「タグ入力」の効果を追加してください。
# 追加してほしい効果
タグ入力(バッジ & アバター)
入力欄にEnterでタグを追加し、×ボタンで削除できるチップ式のタグ入力。大文字小文字を無視して重複を検知し、既存タグを軽く揺らして知らせます。
# 追加する場所
👉【ここに対象箇所を記入:例「トップのヒーローセクション」「お問い合わせボタン」「記事カードの一覧」など】
# 参考実装(この見た目・挙動を再現してください)
【HTML】
<!-- タグ入力: Enterでチップ追加、×で削除、大小無視で重複はシェイクして拒否 -->
<section class="panel" aria-label="タグ入力のデモ">
<label class="field-label" for="tagInput">興味のあるタグ</label>
<div class="tag-field" id="tagField">
<!-- チップはJSがinputの直前に動的挿入する -->
<input
type="text"
id="tagInput"
class="tag-input"
placeholder="タグを入力してEnter"
autocomplete="off"
aria-describedby="tagCount"
>
</div>
<p class="field-hint" id="tagCount" role="status" aria-live="polite">0 個のタグ</p>
</section>
【CSS】
* { box-sizing: border-box; }
body {
margin: 0;
min-height: 100vh;
display: grid;
place-items: center;
padding: 24px;
font-family: "Segoe UI", system-ui, sans-serif;
background: linear-gradient(160deg, #fdfbf6 0%, #f5efe3 55%, #ece2d0 100%);
}
.panel {
width: min(380px, 92vw);
background: #ffffff;
border-radius: 20px;
padding: 26px 26px 22px;
box-shadow: 0 24px 48px -22px rgba(120, 92, 40, .22), 0 2px 8px rgba(60, 45, 20, .06);
border: 1px solid rgba(180, 150, 100, .18);
}
.field-label {
display: block;
margin: 0 0 12px;
font-size: 13px;
font-weight: 700;
color: #57534e;
letter-spacing: .02em;
}
.tag-field {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 8px;
padding: 10px 12px;
border-radius: 14px;
background: #faf7f0;
border: 1.5px solid #e7ddc8;
transition: border-color .2s ease, box-shadow .2s ease;
}
.tag-field:focus-within {
border-color: #d6a94c;
box-shadow: 0 0 0 3px rgba(214, 169, 76, .18);
}
/* チップ(.chip)はtag-field直下にJSで挿入され、inputと同じ行でフローする */
.chip {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 6px 8px 6px 12px;
border-radius: 999px;
background: #fdf0d8;
color: #92660f;
font-size: 13px;
font-weight: 600;
animation: chip-in .28s cubic-bezier(.34, 1.56, .64, 1);
}
.chip__label { line-height: 1; }
.chip__remove {
appearance: none;
border: none;
background: rgba(146, 102, 15, .12);
color: #92660f;
width: 18px;
height: 18px;
border-radius: 50%;
font-size: 12px;
line-height: 1;
cursor: pointer;
display: grid;
place-items: center;
transition: background .15s ease, transform .15s ease;
}
.chip__remove:hover { background: rgba(146, 102, 15, .24); }
.chip__remove:active { transform: scale(.88); }
.chip__remove:focus-visible { outline: 2px solid #d6a94c; outline-offset: 2px; }
/* 重複入力時の拒否フィードバック */
.chip.is-dup { animation: chip-shake .38s ease; }
.tag-input {
flex: 1 1 120px;
min-width: 100px;
border: none;
background: transparent;
font-size: 14px;
color: #3d3a34;
padding: 4px 2px;
}
.tag-input:focus { outline: none; }
.tag-input::placeholder { color: #b7ac96; }
.field-hint {
margin: 12px 2px 0;
font-size: 12px;
color: #a89a78;
}
@keyframes chip-in {
0% { transform: scale(.6); opacity: 0; }
100% { transform: scale(1); opacity: 1; }
}
@keyframes chip-shake {
0%, 100% { transform: translateX(0); }
20% { transform: translateX(-5px); }
40% { transform: translateX(4px); }
60% { transform: translateX(-3px); }
80% { transform: translateX(2px); }
}
@media (prefers-reduced-motion: reduce) {
.chip, .chip.is-dup { animation: none !important; }
}
【JavaScript】
// タグ入力: Enterでチップ追加、×ボタンで削除。大小無視で重複ならシェイクして拒否する。
(() => {
const field = document.getElementById('tagField');
const input = document.getElementById('tagInput');
const count = document.getElementById('tagCount');
if (!field || !input || !count) return; // null安全
let tags = ['デザイン', 'リサーチ']; // 初期表示用のサンプルタグ
const render = () => {
field.querySelectorAll('.chip').forEach((el) => el.remove());
tags.forEach((tag) => {
const chip = document.createElement('span');
chip.className = 'chip';
const label = document.createElement('span');
label.className = 'chip__label';
label.textContent = tag; // textContentでXSSを回避
const remove = document.createElement('button');
remove.type = 'button';
remove.className = 'chip__remove';
remove.setAttribute('aria-label', `${tag} を削除`);
remove.textContent = '×';
remove.addEventListener('click', () => {
tags = tags.filter((t) => t !== tag);
render();
input.focus();
});
chip.append(label, remove);
field.insertBefore(chip, input);
});
count.textContent = `${tags.length} 個のタグ`;
};
const shake = (index) => {
const chips = field.querySelectorAll('.chip');
const target = chips[index];
if (!target) return;
target.classList.remove('is-dup');
void target.offsetWidth; // リフローでアニメを再生成
target.classList.add('is-dup');
};
input.addEventListener('keydown', (e) => {
if (e.key !== 'Enter') return;
e.preventDefault();
const raw = input.value.trim();
if (!raw) return;
const dupIndex = tags.findIndex((t) => t.toLowerCase() === raw.toLowerCase());
if (dupIndex !== -1) {
shake(dupIndex); // 既存タグと重複 -> 追加せず既存チップを揺らして知らせる
input.value = '';
return;
}
tags.push(raw);
render();
input.value = '';
});
render();
})();
# 外部ライブラリ
なし(追加ライブラリ不要)
# 守ってほしいこと
- 既存のHTML構造・レイアウト・デザインを壊さないこと。必要に応じてクラス名・色・サイズを私のサイトに合わせて調整してよい。
- クラス名やidが既存と衝突しないよう、必要なら接頭辞で名前空間を分けること。
- レスポンシブ対応と prefers-reduced-motion への配慮を入れること。
- 私のサイトのフレームワーク(React / Vue / 素のHTML など)に合わせて実装すること。不明な場合は素のHTML/CSS/JSで提示し、組み込み手順も説明すること。
- 変更後の確認手順も簡潔に教えてください。