タグ入力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>
/* 貼った先のリセットに頼らないよう、この部品の中だけ 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%);
}
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 で出す