Tag input

태그 입력

A field where typing a value and pressing Enter turns it into a removable chip, clearing the field for the next one.

Also known as: Token inputChip inputMulti-value input
···
html
<div class="tiw">
  <span class="tilabel">받는 사람</span>
  <div class="tifield" id="tifield">
    <span class="tag">김서연<button aria-label="김서연 삭제">×</button></span>
    <span class="tag">박도윤<button aria-label="박도윤 삭제">×</button></span>
    <input class="tiinput" id="tiinput" type="text" aria-label="받는 사람 추가" placeholder="이름 입력 후 Enter">
  </div>
</div>
css
.tiw{position:absolute;inset:0;display:grid;place-items:center;gap:6px}
.tilabel{color:var(--muted);font-size:10px}
.tifield{width:min(220px,90%);min-height:36px;display:flex;flex-wrap:wrap;gap:5px;align-items:center;border:1px solid var(--line);border-radius:9px;background:var(--surface);padding:5px 7px}
.tag{display:inline-flex;align-items:center;gap:5px;background:var(--bg);border:1px solid var(--line);border-radius:999px;padding:3px 4px 3px 9px;font-size:10px;color:var(--fg);transition:opacity .2s,transform .2s}
.tag[data-gone]{opacity:0;transform:scale(.8)}
.tag button{border:0;background:none;color:var(--muted);font-size:11px;cursor:default;line-height:1;padding:2px}
.tiinput{flex:1;min-width:56px;border:0;background:none;color:var(--fg);font-size:11px;outline:0}
js
const field = document.getElementById('tifield'), input = document.getElementById('tiinput');
function addTag(name) {
  const t = document.createElement('span');
  t.className = 'tag';
  t.appendChild(document.createTextNode(name));
  const b = document.createElement('button');
  b.setAttribute('aria-label', name + ' 삭제');
  b.textContent = '×';
  b.addEventListener('click', () => { auto = false; t.setAttribute('data-gone', ''); setTimeout(() => t.remove(), 200); });
  t.appendChild(b);
  field.insertBefore(t, input);
}
input.addEventListener('keydown', (e) => {
  if (e.key === 'Enter' && input.value.trim()) { auto = false; addTag(input.value.trim()); input.value = ''; }
  if (e.key === 'Backspace' && !input.value) {
    auto = false;
    const tags = field.querySelectorAll('.tag');
    if (tags.length) tags[tags.length - 1].remove();
  }
});
const NAMES = ['이하은', '최서준', '정민재'];
let auto = true, ni = 0;
function typeOut(name, cb) {
  let i = 0;
  const t = setInterval(() => {
    if (!auto) return clearInterval(t);
    input.value = name.slice(0, ++i);
    if (i >= name.length) { clearInterval(t); setTimeout(cb, 400); }
  }, 130);
}
function loop() {
  if (!auto) return;
  const name = NAMES[ni % NAMES.length];
  typeOut(name, () => {
    if (!auto) return;
    addTag(name); input.value = ''; ni++;
    setTimeout(() => {
      if (!auto) return;
      const tags = field.querySelectorAll('.tag');
      if (tags.length > 4) tags[0].remove();
      setTimeout(loop, 900);
    }, 1200);
  });
}
setTimeout(loop, 400);

Typing a value and pressing Enter (or a comma) hardens it into a removable chip, and the field clears for the next one. Each chip's remove button needs a specific label — "Remove Kim Seoyeon" — since an icon alone can't say what's being removed. Pressing Backspace with the cursor at the start of an empty field conventionally deletes the chip right before it.

Its relationship to the chip entry elsewhere in this dictionary is output versus creation — a chip is an already-made, standalone label, while a tag input is the typing mechanism that builds up those chips one at a time. It differs from a combobox in purpose — a combobox fills in one value from a list of candidates and is done, while a tag input keeps accumulating several values into a growing list, even though both often share the same combobox-plus-listbox machinery under the hood.

If autocomplete candidates are offered alongside it, add the listbox pattern so candidates filter as you type and arrow keys can pick one.