Webパーツ辞典

タグ入力Tag input

文字を入れて Enter を押すと、その語が × 付きの小さな札になって枠の中に並ぶ入力欄。記事のタグやメールの宛先のように、短い語をいくつも入れてもらうときに使う。

表示例

  • CSS
  • アクセシビリティ

Enter か「,」で追加。5件まで(いま2件)

アイコン: Font Awesome 7 Free(fa-solid / fa-regular)

別名・英語表記

  • トークン入力
  • チップ入力
  • タグ付け入力欄
  • キーワードの複数入力
  • タグエディター
  • Tag input

実装例

<div class="x-tag-input">
  <label class="x-tag-input__l" for="x-tag-input-i">記事のタグ</label>
  <!-- 枠のどこを押しても入力欄へフォーカスが移るよう、実際は枠のクリックで input.focus() を呼ぶ -->
  <div class="x-tag-input__box">
    <!-- list-style: none の ul は Safari がリストとして読まないので role="list" を足す -->
    <ul class="x-tag-input__list" role="list" aria-label="追加したタグ">
      <li class="x-tag-input__t">CSS<button class="x-tag-input__x" type="button" aria-label="CSS を削除"><i class="fa-solid fa-xmark" aria-hidden="true"></i></button></li>
      <li class="x-tag-input__t">アクセシビリティ<button class="x-tag-input__x" type="button" aria-label="アクセシビリティ を削除"><i class="fa-solid fa-xmark" aria-hidden="true"></i></button></li>
    </ul>
    <input class="x-tag-input__i" id="x-tag-input-i" type="text" name="tag" value="フォーム"
           autocomplete="off" aria-describedby="x-tag-input-h">
  </div>
  <p class="x-tag-input__h" id="x-tag-input-h">Enter か「,」で追加。5件まで(いま2件)</p>
  <!-- 追加・削除のたびに「アクセシビリティ を追加しました」のように書き換える -->
  <p class="x-tag-input__sr" role="status"></p>
</div>

AIへの指示文

「タグ入力(Tag input)」を実装してください。

## これは何か
文字を入れて Enter を押すと、その語が × 付きの小さな札になって枠の中に並ぶ入力欄。記事のタグやメールの宛先のように、短い語をいくつも入れてもらうときに使う。

## 使いどころ
- 記事のタグ・スキル・興味のある分野のように、短い語を何個でも入れてもらうとき
- メールの宛先のように、1件ずつ確定させて間違いを見つけやすくしたいとき
- オートコンプリートと組み合わせて、既存のタグから選ばせつつ新しい語も足せるようにしたいとき

## 実装の要件
- label + div(枠)> ul[aria-label] > li(語 + button[aria-label])+ input、その下に補足文(aria-describedby)と role="status" の領域
- 枠のどこを押しても input にフォーカスが移るよう、枠のクリックで input.focus() を呼ぶ
- list-style: none の ul は Safari がリストとして読まないので role="list" を足す
- 何のキーで確定するのかを欄の近くに書く。Enter だけにするか、「,」や「、」でも区切るかを決めて伝える
- 「,」「、」で区切るなら keydown ではなく input イベントで値を見て区切り、event.isComposing が true の間は区切らず compositionend のあとで区切る。日本語入力では「,」のキーが「、」として変換中の文字に入り、keydown は keyCode 229 で届く
- 日本語の変換中(IME の確定前)の Enter で札にしない。keydown で event.isComposing が true か event.keyCode が 229 のときは無視する。Safari 26 以前は確定の Enter で isComposing が false になるので、keyCode 229 の確認を外さない
- 空の欄で Backspace を押すと最後の札が消える作りは、押しすぎて消えやすい。1回目は最後の札を選んだ状態にし、2回目で消すと戻しやすい
- 同じ語の重複、前後の空白、上限の件数と文字数を決めておく。弾いたときは理由を文字で伝える
- 送信する値の形を決める。札ごとに hidden の input を置くか、区切り文字でつないだ1つの値にする

## アクセシビリティの要件
- 入力欄には label を for / id で結ぶ。札の並びは ul に aria-label(「追加したタグ」)を付けて、何件あるかが伝わるようにする
- 札の削除ボタンは button にし、aria-label で「CSS を削除」のように対象の語を入れる。× だけでは何を消すのか分からない
- 札を足した・消したことを role="status" の領域で「CSS を追加しました」と控えめに伝える。見た目の変化だけでは、読み上げでは気づけない
- 削除ボタンで札を消したら、フォーカスを入力欄か隣の札のボタンへ移す。消えた要素に残すと body へ落ちる
- 枠の中の input は枠線を持たないので、フォーカスの印は外側の枠に :focus-within で出す

## 参考にする実装
この形を出発点にして、プロジェクトの命名規則・色・余白に合わせて調整してください。

```html
<div class="x-tag-input">
  <label class="x-tag-input__l" for="x-tag-input-i">記事のタグ</label>
  <!-- 枠のどこを押しても入力欄へフォーカスが移るよう、実際は枠のクリックで input.focus() を呼ぶ -->
  <div class="x-tag-input__box">
    <!-- list-style: none の ul は Safari がリストとして読まないので role="list" を足す -->
    <ul class="x-tag-input__list" role="list" aria-label="追加したタグ">
      <li class="x-tag-input__t">CSS<button class="x-tag-input__x" type="button" aria-label="CSS を削除"><i class="fa-solid fa-xmark" aria-hidden="true"></i></button></li>
      <li class="x-tag-input__t">アクセシビリティ<button class="x-tag-input__x" type="button" aria-label="アクセシビリティ を削除"><i class="fa-solid fa-xmark" aria-hidden="true"></i></button></li>
    </ul>
    <input class="x-tag-input__i" id="x-tag-input-i" type="text" name="tag" value="フォーム"
           autocomplete="off" aria-describedby="x-tag-input-h">
  </div>
  <p class="x-tag-input__h" id="x-tag-input-h">Enter か「,」で追加。5件まで(いま2件)</p>
  <!-- 追加・削除のたびに「アクセシビリティ を追加しました」のように書き換える -->
  <p class="x-tag-input__sr" role="status"></p>
</div>
```

```css
/* 貼った先のリセットに頼らないよう、この部品の中だけ border-box にする */
.x-tag-input,
.x-tag-input *:not([class*="fa-"]),
.x-tag-input *:not([class*="fa-"])::before,
.x-tag-input *:not([class*="fa-"])::after {
  box-sizing: border-box;
  /* 貼った先の既定の書体に引きずられないようにする。書体が変わると
     文字の幅と高さが変わり、枠のサイズまでずれる */
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Hiragino Sans", "Noto Sans JP", "Yu Gothic UI", Meiryo, sans-serif;
}

.x-tag-input {
  width: 300px;
  color: #1b2028;
}
.x-tag-input__l {
  display: block;
  margin-bottom: 6px;
  font-size: 12px;
  font-weight: 700;
}
/* 枠は入力欄ではなく外側の div が持つ。フォーカスは中の input から :focus-within で拾う */
.x-tag-input__box {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 6px;
  min-height: 44px;
  padding: 6px 8px;
  border: 1px solid #7f8a99;
  border-radius: 8px;
  background: #fff;
  cursor: text;
}
.x-tag-input__box:focus-within {
  outline: 2px solid #1d4ed8;
  outline-offset: 1px;
}
.x-tag-input__list {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  margin: 0;
  padding: 0;
  list-style: none;
}
.x-tag-input__t {
  display: inline-flex;
  align-items: center;
  gap: 2px;
  height: 28px;
  padding: 0 2px 0 10px;
  border: 1px solid #1d4ed8;
  border-radius: 999px;
  background: #eef2ff;
  color: #1d4ed8;
  font-size: 12px;
}
.x-tag-input__x {
  width: 24px;
  height: 24px;
  border: 0;
  border-radius: 50%;
  background: none;
  color: inherit;
  font-size: 11px;
  cursor: pointer;
}
.x-tag-input__x:hover {
  background: #dbe4ff;
}
.x-tag-input__x:focus-visible {
  outline: 2px solid #1d4ed8;
  outline-offset: 0;
}
/* 入力欄は枠を持たず、残りの幅を埋める */
.x-tag-input__i {
  flex: 1 1 80px;
  min-width: 80px;
  height: 28px;
  padding: 0 4px;
  border: 0;
  outline: none;
  background: none;
  color: #1b2028;
  font-size: 14px;
}
.x-tag-input__h {
  margin: 6px 0 0;
  color: #5b6472;
  font-size: 11px;
}
.x-tag-input__sr {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: 0;
  overflow: hidden;
  white-space: nowrap;
  clip-path: inset(50%);
}
```

## 出力の約束
- HTML と CSS を分けて出す。JavaScript が要るときは素の JavaScript で書き、UIフレームワークは使わない。
- 参考実装がアイコンに Font Awesome のクラスを使っている場合は、プロジェクトで使っているアイコンかインライン SVG に置き換えてよい。
- 上のアクセシビリティ要件を満たす。満たせない箇所があるときは、理由を添えて先に知らせる。
- 色・角丸・余白は既存のデザイントークンがあればそれを使う。無ければ CSS 変数として切り出す。

使いどころ

  • 記事のタグ・スキル・興味のある分野のように、短い語を何個でも入れてもらうとき
  • メールの宛先のように、1件ずつ確定させて間違いを見つけやすくしたいとき
  • オートコンプリートと組み合わせて、既存のタグから選ばせつつ新しい語も足せるようにしたいとき

注意点

  • 何のキーで確定するのかを欄の近くに書く。Enter だけにするか、「,」や「、」でも区切るかを決めて伝える
  • 「,」「、」で区切るなら keydown ではなく input イベントで値を見て区切り、event.isComposing が true の間は区切らず compositionend のあとで区切る。日本語入力では「,」のキーが「、」として変換中の文字に入り、keydown は keyCode 229 で届く
  • 日本語の変換中(IME の確定前)の Enter で札にしない。keydown で event.isComposing が true か event.keyCode が 229 のときは無視する。Safari 26 以前は確定の Enter で isComposing が false になるので、keyCode 229 の確認を外さない
  • 空の欄で Backspace を押すと最後の札が消える作りは、押しすぎて消えやすい。1回目は最後の札を選んだ状態にし、2回目で消すと戻しやすい
  • 同じ語の重複、前後の空白、上限の件数と文字数を決めておく。弾いたときは理由を文字で伝える
  • 送信する値の形を決める。札ごとに hidden の input を置くか、区切り文字でつないだ1つの値にする

アクセシビリティの要点

  • 入力欄には label を for / id で結ぶ。札の並びは ul に aria-label(「追加したタグ」)を付けて、何件あるかが伝わるようにする
  • 札の削除ボタンは button にし、aria-label で「CSS を削除」のように対象の語を入れる。× だけでは何を消すのか分からない
  • 札を足した・消したことを role="status" の領域で「CSS を追加しました」と控えめに伝える。見た目の変化だけでは、読み上げでは気づけない
  • 削除ボタンで札を消したら、フォーカスを入力欄か隣の札のボタンへ移す。消えた要素に残すと body へ落ちる
  • 枠の中の input は枠線を持たないので、フォーカスの印は外側の枠に :focus-within で出す

作るときに使えるツール

見分け方

関連するパーツ