태그 입력

Tag input

입력창에 값을 치고 Enter를 누르면 지워지지 않는 칩으로 쌓이고, 입력창은 비워져 다음 값을 받는 필드.

다른 이름: 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);

입력창에 값을 치고 Enter나 콤마를 누르면 그 값이 지워지지 않는 칩(태그)으로 굳어지고, 입력창은 다시 비워져 다음 값을 받습니다. 각 태그의 삭제 버튼에는 "김서연 삭제"처럼 구체적인 라벨을 달아야 아이콘만으론 무엇이 지워지는지 알 수 없는 문제를 피합니다. 커서가 입력창 맨 앞에 있을 때 Backspace를 누르면 바로 앞 태그가 지워지는 것도 관례입니다.

칩(이 사전의 다른 항목)과의 관계는 결과물과 생성 과정의 차이입니다 — 칩은 이미 만들어진 낱개의 독립 라벨을 다루고, 태그 입력은 타이핑으로 그 칩들을 하나씩 만들어 쌓아가는 입력 메커니즘입니다. 콤보박스와는 목적이 다릅니다 — 콤보박스는 후보 중 값 하나를 골라 채우는 게 끝이지만, 태그 입력은 여러 값을 계속 추가해 누적된 목록을 만드는 게 목적이라 같은 콤보박스+리스트박스 조합을 내부적으로 쓰면서도 겉보기 동작이 다릅니다.

자동완성 후보를 함께 제공한다면 리스트박스 패턴을 더해 입력 중 후보를 필터링하고 방향키로 고를 수 있게 합니다.