メインコンテンツへスキップ
menumenu tanreiexpand_morecontrasttranslate
tanreiexpand_more
はじめにWebReactSwiftUIJetpack ComposeFlutterReact Nativeプリセット自分のデザインシステムを作るAI エージェントと Tanrei を使うブランドデザイントークンデザインシステムとカスタマイズTheme Studioカラータイポグラフィアイコン形状エレベーションモーションインタラクション状態振る舞いレイアウトアクセシビリティ言葉カラートークンタイプスケールトークン形状トークンエレベーショントークンモーショントークン状態トークンスペースと寸法のトークンコンポーネントボタンメインアクションボタンセグメントボタンボタングループメニュー付きボタンツールバーコピーボタンチェックボックスラジオボタンスイッチチップメニュースライダースライダーフィールドカレンダーDateTimePickerフィールドグループラジオグループチェックボックスグループセレクトセレクトフィールドメニューバーファイルアップロードファイルピッカー 上級者向けドロップゾーンテキスト入力欄 上級者向けテキストフィールドチップフィールドフィールドコンボボックス 上級者向けコンボボックスフィールド数値入力欄 上級者向け数値フィールドカラー入力欄 上級者向けカラーフィールドワンタイムコード入力欄 上級者向けワンタイムコードフィールド埋め込みテキスト入力欄 上級者向けチャットコンポーザーカード区切り線モーダルレイヤー 上級者向けダイアログリストリスト行ボトムシートサイドシートカルーセルコードブロックテーブルポップオーバーステップアコーディオン説明リスト図アプリシェルページヘッダーツリービュータイムライン画像プレビューハンドルサイズ変更できる枠スクロールエリアバッジプログレスインジケーターメーターナンバーティッカーローディングインジケータースナックバーツールチップキーボードショートカットタグインラインコードコールアウトアバター空の状態タググループスケルトンファイル項目色見本マーキーテキストタブタブビューアプリバーナビゲーションバーナビゲーションドロワー検索リンクページネーション前後のリンクステッパーパンくずリスト目次リンクカードFAB メニューステータスTanrei の作り方コントリビューションライセンスと帰属
ホームコンポーネントタググループ

タググループ

タグやチップの集まりを、折り返す行に並べ、中身に合った間隔を取り、1 つの名前付きのリストとして伝えます。メンバーはそのままの姿で、チップが取り除かれた後は、フォーカスが次のチップへ進みます。

content_copyMarkdown で表示
カテゴリ
コミュニケーション
トークン
comp.tag-group
レベル
基礎

概要

Web
tanrei-tag-group
schedule
SwiftUI
TanreiTagGroup
radio_button_unchecked
Jetpack Compose
TanreiTagGroup
radio_button_unchecked
Flutter
TanreiTagGroup
radio_button_unchecked
React Native
TagGroup
radio_button_unchecked

生成元 docs/status/status.json, specs/tag-group.spec.json

タグの集まりと、絞り込みの並び

タグもチップも、1 つずつ置くものです。タググループは、それらをいくつかまとめて置くためのものです。記事のタグ、一覧の上の絞り込み、フィールドの下に出すいくつかの候補などに使います。メンバーを折り返す行に並べ、メンバーの間に適切な間隔を取り、全体が 1 つのリストであることを支援技術に伝えます。

グループ自身は何も描きません。コンテナも、縁も、パディングも、目に見えるラベルもありません。メンバーは、ほかの場所に置いたときとまったく同じように描かれ、フォーカスを受け、押されます。

Web では <tanrei-tag-group> です。ほかのプラットフォームはまだ実装していません。

使い方

<!-- 静的なタグの集まりは、そのことを指定します。行と行が、タグ同士と同じ近さで並びます。 -->
<tanrei-tag-group spacing="tags" label="タグ">
  <tanrei-tag label="デザイントークン"></tanrei-tag>
  <tanrei-tag label="アクセシビリティ"></tanrei-tag>
</tanrei-tag-group>

<!-- チップの集まりは既定の間隔を使います。行と行は、それぞれのチップのタッチターゲットが収まるだけ離れます。 -->
<tanrei-tag-group id="recipients" label="宛先">
  <tanrei-chip kind="input" label="山田 花子" remove-label="山田 花子を削除"></tanrei-chip>
  <tanrei-chip kind="input" label="佐藤 健" remove-label="佐藤 健を削除"></tanrei-chip>
</tanrei-tag-group>

<script type="module">
  import '@tanrei/web/components/tag-group';
  import '@tanrei/web/components/tag';
  import '@tanrei/web/components/chip';

  const recipients = document.querySelector('#recipients');
  // チップが自分を取り除くことはありません。利用者側で取り除くと、グループがフォーカスを次のチップへ移します。
  recipients.addEventListener('remove', (event) => {
    event.target.remove();
    // 最後の 1 つがなくなると、グループにはフォーカスの行き先がありません。集まりが属するコントロールへ移します。
    if (!recipients.children.length) document.querySelector('#add-recipient').focus();
  });
</script>
import { Chip, TagGroup } from '@tanrei/react';

<TagGroup label="宛先">
  {recipients.map((person) => (
    <Chip key={person.id} kind="input" label={person.name} removeLabel={`${person.name}を削除`} onRemove={() => remove(person.id)} />
  ))}
</TagGroup>

仕様

折り返すタグ、ステータスのタグ、各タグのページへのリンク、入力された項目

構成要素

パーツ 説明
1 group The set: lines of members that start at the start edge and wrap. It draws nothing of its own — no container, no fill, no edge, no padding and no label — and is exposed as one list.
2 member A tag or a chip of the set: placed and spaced by the group, exposed as an item of its list, and otherwise drawn and behaving as it does anywhere.

プロパティ

プロパティ 型 既定値 説明
spacing tags | chips "chips" What the set holds, which decides the distances between its members. tags: static tags, as close as words of one group. chips: chips, which take input, with lines far enough apart that each chip keeps its whole touch target.
label string "" The accessible name of the set ("Tags", "Filters", "Suggested fonts"). It is not drawn: a set that needs a visible name stands under the product's own heading or text, and still takes this. Optional where the set is plain from what surrounds it, recommended for a set of chips.
selection none | single | multiple "none" Whether the group holds the selection of its filter chips. none: it holds none, and each filter chip holds its own selected, as alone. single: at most one filter chip is on at a time, named by value. multiple: any number are on, named by values. While it is single or multiple the set is one Tab stop and the arrow keys move between its chips.
value string null The value of the filter chip that is on while selection is single; null puts none on. The group sets it when a person switches a chip and emits change; the product may also set it.
values strings The values of the filter chips that are on while selection is multiple: a list of strings (the platforms take a set). An empty list puts none on.

イベント

イベント 説明
change While selection is single or multiple, a person switched a filter chip of the set: carries value (single) or values (multiple) after the change, which the group now holds. In place of the chip's own change, which stops at the group (TGR-19).

アイテム

このコンポーネントが繰り返すアイテムの種類です。フィクスチャはアイテムを順に並べ、各プラットフォームはそれぞれの流儀で種類ごとに名前を付けます。

tag

A Tag of the set: the tag component, placed and spaced by the group and exposed as an item of its list, otherwise drawn and read as it is anywhere.

プロパティ 型 既定値 説明
label string "" Its text, as the Tag's label.
variant tonal | outlined | filled "tonal" Its look, as the Tag's variant.
status default | info | warning | error | success "default" What it reports, as the Tag's status.
size small | medium "medium" Its height, as the Tag's size. Give every member of a group the same size.
icon string "" The glyph in the Tag's icon slot, by its name in the system's icon set. Empty for none. It is how a fixture fills that slot; in an application the slot is filled as the Tag's own spec says.
仕様 Web SwiftUI Jetpack Compose Flutter React Native
アイテム tanrei-tag TanreiTag TanreiTag TanreiTag Tag
label · プロパティ label title label label label
variant · プロパティ variant variant variant variant variant
status · プロパティ status status status status status
size · プロパティ size size size size size
icon · プロパティ slot="icon" icon icon icon icon
備考 A tanrei-tag child, as anywhere. A TanreiTag child, as anywhere. A TanreiTag call in the group's content, as anywhere. A TanreiTag in children, as anywhere. A Tag child, as anywhere.

chip

A Chip of the set: the chip component, placed and spaced by the group and exposed as an item of its list, otherwise drawn and behaving as it does anywhere, with its own state, events and Tab stop.

プロパティ 型 既定値 説明
kind action | filter | input "action" What it does, as the Chip's kind.
label string "" Its text and accessible name, as the Chip's label.
size small | medium "medium" Its height, as the Chip's size. Give every member of a group the same size.
selected boolean false Whether a filter chip is on, as the Chip's selected. While selection is none the chip holds it; while selection is single or multiple the group sets it from value or values (TGR-17).
value string "" What the group's value and values name a filter chip by, as the Chip's value. Unique within the set; needed while selection is single or multiple.
disabled boolean false As the Chip's disabled: one chip of a set may be disabled by itself.
href string "" The destination of an action chip, as the Chip's href: it makes the chip a link.
removeLabel string "" The accessible name of an input chip's remove action, as the Chip's removeLabel.
icon string "" The glyph in the Chip's icon slot, by its name in the system's icon set. Empty for none. It is how a fixture fills that slot; in an application the slot is filled as the Chip's own spec says.
イベント 説明
click The chip was activated, as the Chip's click.
change A filter chip was switched, as the Chip's change. While selection is single or multiple it stops at the group, whose own change carries the selection (TGR-19).
remove The person asked to remove an input chip, as the Chip's remove. The product removes it; the group then places the rest and moves focus (TGR-9).
仕様 Web SwiftUI Jetpack Compose Flutter React Native
アイテム tanrei-chip TanreiChip TanreiChip TanreiChip Chip
kind · プロパティ kind isSelected / onRemove onSelectedChange / onRemove TanreiChip / TanreiChip.filter / TanreiChip.input kind
label · プロパティ label title label label label
size · プロパティ size size size size size
selected · プロパティ selected isSelected selected selected selected
value · プロパティ value value value value value
disabled · プロパティ disabled disabled enabled isEnabled disabled
href · プロパティ href destination href href href
removeLabel · プロパティ removeLabel removeLabel removeContentDescription removeSemanticLabel removeAccessibilityLabel
icon · プロパティ slot="icon" icon icon icon icon
click · イベント click action onClick onPressed onPress
change · イベント change isSelected onSelectedChange onSelected onSelectedChange
remove · イベント remove onRemove onRemove onDeleted onRemove
備考 A tanrei-chip child, as anywhere; remove-label is its attribute. The page removes the element on remove; the group sees it leave and moves focus (TGR-9). A TanreiChip child, as anywhere. A TanreiChip call in the group's content, as anywhere; enabled is disabled's inverse. A TanreiChip in children, as anywhere. Give each a Key, so that the group can tell which one left. A Chip child, as anywhere. Give each a key, so that the group can tell which one left.

状態

振る舞い

EARS 記法による要件です。各プラットフォームの実装は、この ID に対応するテストで検証されます。要件の原文は仕様ファイルのものをそのまま掲載しています。

ID 要件
TGR-1 THE tag group SHALL lay out its members in lines: from its start edge, in the order they were given, comp.tag-group.gap.tags apart along a line WHILE spacing is tags and comp.tag-group.gap.chips apart WHILE spacing is chips, each member at its own size and centred on the middle of its line. It SHALL be as wide as its layout gives it and as tall as its lines, SHALL draw no container, fill, edge or elevation of its own, SHALL add no padding around its members, and SHALL NOT draw its label.
TGR-2 WHEN a member does not fit in what is left of a line THE tag group SHALL put it, whole, at the start of a new line below, comp.tag-group.row-gap.tags below the line above WHILE spacing is tags and comp.tag-group.row-gap.chips WHILE spacing is chips, measured between the visible bounds of the tallest members of the two lines; it SHALL NOT cut, shrink or split a member to keep it on a line, SHALL NOT scroll its lines, and SHALL NOT hide a member or stand a count in its place: every member it is given is shown. WHERE one member alone is wider than the group THE tag group SHALL give it the group's width on a line of its own, and the member SHALL then cut its own label as its own spec says (TAG-5, CHP-17).
TGR-3 THE tag group SHALL keep its members in the order they were given: it SHALL NOT sort them, move a selected chip forward or group them by kind, and the order in which focus moves through its chips and in which assistive technology reads its members SHALL be that order — along each line from its start, and line after line from the top.
TGR-4 THE tag group SHALL take from spacing only the distances of TGR-1 and TGR-2: spacing SHALL NOT change what a member is, how it is drawn or what the group exposes. WHERE the platform can tell what its members are, WHILE spacing is tags and a member takes input (a chip) THE tag group SHALL still render and in debug builds report that a set of chips wants spacing chips, because the lines of tags stand closer than a chip's touch target is high.
TGR-5 WHILE the hit areas that two neighbouring chips extend beyond their visible bounds (CHP-5, CHP-21) overlap THE tag group SHALL give every press on a chip's visible bounds to that chip, and a press in the gap between two chips — along a line or between two lines — to the nearer one, so that neighbouring hit areas meet at the middle of the gap. comp.tag-group.row-gap.chips is what keeps each chip's target whole between two lines: at sys.target.touch minus a chip's height or more, no hit areas overlap, as under the default system's value for a medium chip; a small chip on wrapped lines, and a layer that narrows the gap, have targets that meet between the lines.
TGR-6 THE tag group SHALL NOT restyle its members: each SHALL render from its own tokens at its own size, answer hover, focus and press itself, hold its own state (a filter chip its selected, which the group sets WHILE selection is single or multiple, TGR-17), emit its own events (but the change of a filter chip the group holds, TGR-19) and keep its own role and name inside the item the group exposes it as (TGR-7). WHILE selection is none each chip SHALL be its own Tab stop — an input chip one, the chip itself, whose remove action is not a stop and which Backspace or Delete removes (CHP-18, CHP-19, CHP-23) — in the order of TGR-3, and the tag group SHALL NOT handle arrow keys, Home or End, SHALL keep no selection of its own, SHALL NOT make its filter chips exclusive, and SHALL emit no event; what a selection changes is TGR-17 to TGR-21. The tag group itself SHALL NOT take focus, whatever its selection. The tag group has no size of its own: members of different sizes on one line are centred on it (TGR-1).
TGR-7 WHILE the tag group holds at least one member THE tag group SHALL expose itself to assistive technology as a list, named by label WHILE label is not empty, and each member as one item of that list, in the order of TGR-3, holding the member as the member exposes itself: a tag as its text (TAG-11), a chip as its button or its link with its state, an input chip with its remove action beside it. WHEN label changes THE tag group SHALL expose the new name at once. WHILE label is empty THE tag group SHALL still expose the list, unnamed. A platform that has no list role SHALL expose a container named by label that holds the members in order, and state that in its README.
TGR-8 WHILE the tag group holds no member THE tag group SHALL draw nothing, take no space and expose nothing — no empty list and no name — and SHALL report nothing: a set that has no tags yet, or whose last filter was removed, is not a mistake. WHILE it holds one member THE tag group SHALL lay it out and expose it as a list of one.
TGR-9 WHEN a member leaves the tag group while focus is on it — on a chip or on its remove action — THE tag group SHALL move focus to the chip that follows it in the order of TGR-3, or to the chip before it WHERE none follows, on the chip itself and never on its remove action, so that a person who removes several entered items in a row goes on from where they were; it SHALL skip a member that takes no focus (a tag, a disabled chip). WHERE no member that takes focus is left THE tag group SHALL NOT move focus, having nowhere of its own to put it: the product moves it to the control that the set belongs to (CHP-22). THE tag group SHALL NOT remove a member itself and SHALL announce nothing.
TGR-10 WHEN a member is added, is removed or changes its size THE tag group SHALL place its members again by TGR-1 and TGR-2 and SHALL move every member whose place that changes from where it was drawn to its new place over comp.tag-group.motion.move-duration with comp.tag-group.motion.move-easing, along its line and from one line to another, without fading or scaling it; a change that comes while members move SHALL take each of them on from where it is drawn. A member that is added SHALL appear in its place at once and one that is removed SHALL leave at once. WHILE a line holds the same members in the same order as before the change, its members SHALL follow the change at once: a member that changes its size, at once or over a motion of its own, carries the members after it on its line, and a line whose height changes carries the lines below. THE tag group's own height SHALL follow its lines at once, and WHEN the width the layout gives the tag group changes THE tag group SHALL place its members again at once. WHERE motion is reduced THE tag group SHALL place its members again at once. A member that has focus while it moves SHALL keep it, its focus indicator moving with it, and THE tag group SHALL NOT move focus because a member was added.
TGR-11 WHILE the tag group holds content that is not a Tanrei tag or chip THE tag group SHALL still lay it out and expose it as a member, and in debug builds report it; a platform that cannot inspect its content SHALL state the rule in its README and docs page.
TGR-12 WHILE the text size is enlarged THE tag group SHALL keep its gaps and let its members grow as their own specs say (TAG-13, CHP-17), wrapping onto as many lines as they then need; at 200% no member SHALL be lost, lie over another or be cut by the group, and the group SHALL need scrolling in no direction of its own.
TGR-13 WHERE the platform draws with a person's own forced colors (the web's forced-colors mode, and a platform's equivalent where it has one) THE tag group SHALL draw nothing of its own there either: its members draw themselves as their own specs say (TAG-14), and the distances of TGR-1 and TGR-2 stay what tells them apart.
TGR-14 WHERE the layout direction is right-to-left THE tag group SHALL start every line at the right edge and place its members from right to left, in the order they were given, wrapping to a new line below as TGR-2 says; the order of focus and of reading SHALL stay the order of TGR-3. Each member's own content SHALL follow the layout direction as the member's spec says (TAG-15, CHP-25).
TGR-15 THE tag group SHALL take its layout direction from where it is used — the direction of the nearest ancestor that sets one, and the platform's own where none does — and SHALL have no direction setting of its own; WHEN that direction changes while the tag group is shown THE tag group SHALL draw itself for the new direction at once, without a motion of its own, keeping its members, their state and their focus.
TGR-16 WHEN spacing names a value that the active design system does not have THE tag group SHALL render with the default value's tokens, and in debug builds report the unknown value.
TGR-17 WHILE selection is single or multiple THE tag group SHALL hold the selection of its filter chips by their value: it SHALL set each filter chip's selected to whether value equals the chip's value (single) or values holds it (multiple) WHEN selection, value or values changes, WHEN a filter chip arrives and WHEN a chip's value or kind changes, so that what each chip shows and exposes as its pressed state (CHP-11) is always the group's selection. A chip that is not a filter chip, a tag and other content take no part in it, and are laid out, reached and exposed as WHILE selection is none. WHEN value or values names no member THE tag group SHALL put nothing on for that name and report nothing, because a filter whose chip was taken away is not a mistake; WHEN a member leaves THE tag group SHALL leave value and values as they are and emit nothing. WHEN selection becomes none THE tag group SHALL leave each chip's selected as it then is and hold it no longer. WHEN selection names a value it does not have THE tag group SHALL behave as WHILE it is none, and in debug builds report the value.
TGR-18 WHILE selection is single or multiple, WHERE a filter chip's value is empty THE tag group SHALL leave that chip out of its selection — it keeps its own selected and its own change, as WHILE selection is none — and WHERE two filter chips share a value THE tag group SHALL put them on and off together, because they name one thing; in debug builds it SHALL report each, and it SHALL render either way.
TGR-19 WHEN the person switches a filter chip that has a value — by pointer, touch, Enter, Space or assistive technology — WHILE selection is single THE tag group SHALL set value to the chip's value WHERE the chip is now on, and switch every other filter chip off, or to null WHERE it is now off, because each chip is a toggle that says it is pressed and a pressed toggle that cannot be released would be untrue; WHILE selection is multiple THE tag group SHALL add the chip's value to values WHERE the chip is now on, or take it out WHERE it is now off. THE tag group SHALL then emit change exactly once, carrying value (single) or values (multiple), which it already holds, and SHALL stop the chip's own change at the group, so that a listener on the group or above it hears one change that carries the selection. THE tag group SHALL emit nothing WHEN the product sets selection, value or values.
TGR-20 WHILE selection is single or multiple and the group holds a chip that takes focus THE tag group SHALL be one stop in the focus order, in place of the stops TGR-6 gives each chip: the chip that last had focus WHILE it is still in the group and takes focus, otherwise the first filter chip that is on, otherwise the first chip that takes focus; every other chip, and with it its remove action, SHALL be out of the Tab order and stay focusable by the keys of this rule, by pointer and touch, and by assistive technology. WHILE a chip of the group has focus THE tag group SHALL move focus, without switching or removing anything: on Right Arrow to the chip drawn after it and on Left Arrow to the one drawn before it — in a right-to-left layout the same two keys the other way, so that an arrow always moves the way it points — on Home to the first chip and on End to the last, passing every member that takes no focus (a tag, a disabled chip); focus SHALL NOT wrap, and Up Arrow and Down Arrow SHALL do nothing: the members are one row in their order, however many lines draw it. Enter and Space SHALL stay the focused chip's own — Space switches a filter chip (CHP-11) — and so SHALL Backspace and Delete on an input chip (CHP-19).
TGR-21 WHILE selection is single or multiple, WHEN a member leaves while focus is on it THE tag group SHALL move focus as TGR-9 says, and the chip that then has focus SHALL be the set's stop; WHEN the chip that was the set's stop leaves, is disabled or no longer takes focus while focus is elsewhere THE tag group SHALL make a stop as TGR-20 says of a group whose last-focused chip is gone, so that the set keeps exactly one stop while it holds a chip that takes focus. WHEN selection becomes none THE tag group SHALL give each chip its own stop again.
TGR-22 WHERE the platform has ARIA attributes (the web), WHEN the product sets an aria- attribute on the host, by the attribute or by the ARIA property that reflects it, THE tag group SHALL move it to the data-aria- attribute of the same name, so that the host is no second node with its name, description or state, and SHALL expose it on its list — an id reference by the elements it names in the host's tree or a tree around it, a description after its own. WHILE label is not empty it is the list's name, over a name given so; WHILE label is empty, a name given so is the list's name. WHEN the product removes the aria- attribute or its data-aria- twin, or sets the ARIA property that reflects it (ariaLabel, ariaDescribedByElements…) to null, THE tag group SHALL take it off that element.

プラットフォーム別の名前対応

パリティは API が同一であることを意味しません。同じ契約を、各プラットフォームの流儀で表現します。

仕様 Web SwiftUI Jetpack Compose Flutter React Native
コンポーネント tanrei-tag-group TanreiTagGroup TanreiTagGroup TanreiTagGroup TagGroup
spacing · プロパティ spacing spacing spacing spacing spacing
label · プロパティ label label contentDescription semanticLabel accessibilityLabel
selection · プロパティ selection selectionMode selectionMode selectionMode selectionMode
value · プロパティ value selection selectedValue selectedValue value
values · プロパティ values selection selectedValues selectedValues values
change · イベント change selection onSelectionChange onSelectionChanged onSelectionChange
備考 <tanrei-tag-group label="Filters" spacing="chips"> holds tanrei-tag and tanrei-chip children. The members stay in the product's light DOM, in the default slot: they keep their attributes, events, styles and place in the document, and the group moves none of them. The shadow root holds one element with role="list" and aria-label, a flex container that wraps (flex-wrap: wrap; align-items: center; column-gap; row-gap from the tokens of spacing), around the slot; the host has no role, and the group gives each slotted member role="listitem" where it has no role of its own and takes it back when the member leaves (slotchange), as the carousel exposes its items. A tanrei-chip host carries no role itself — its button or link is in its shadow root — so the item holds that control. While the slot is empty the host is display: none and the list is not rendered (TGR-8). Removal: a MutationObserver and slotchange find a member that left while :focus-within held on it, and the group calls focus() on the next chip (TGR-9). The debug report of TGR-4 looks for a tanrei-chip among the slotted elements. Before the element is defined its children are already shown, as the inline elements they are, in source order and separated by the white space between them: a set of tags is readable and a set of chips is operable once the chips themselves are defined; a page that wants the final layout from the first paint adds tanrei-tag-group:not(:defined) { display: flex; flex-wrap: wrap; gap: … } with the token's custom properties. The host is display: block. Selection (TGR-17 to TGR-21): selection and value are attributes and properties; values is a property (an array), and the values attribute is also read, as JSON or as space-separated words, because the fixture harness writes attributes. A chip's value is the tanrei-chip's own value. While selection is single or multiple the group writes each filter chip's selected, stops the chip's change at the host and dispatches its own change (detail.value or detail.values), and gives every chip but the set's stop tabindex="-1" (a host that delegates focus with a negative tabindex is out of the Tab order, with everything in its shadow root), taking back only the tabindex it gave; it handles the arrow keys, Home and End from a keydown listener on the host. TanreiTagGroup(_ label: String = "", spacing: .chips) { TanreiChip(…) … }: a custom Layout that flows its subviews into lines by TGR-1 and TGR-2 and follows layoutDirection. The container is .accessibilityElement(children: .contain) with the label; SwiftUI has no list trait for an arbitrary layout, so VoiceOver reads the label and then each member (TGR-7's fallback). Focus after a removal uses @AccessibilityFocusState and @FocusState keyed by the members' identities, which the caller gives with .id(_:) as in a ForEach. Selection: TanreiTagGroup(selection: $value) with a Binding<String?> (single) or a Binding<Set<String>> (multiple), each chip given its value as TanreiChip(value:); the arrow keys come from focusable() and onMoveCommand. TanreiTagGroup(spacing = TanreiTagGroupSpacing.Chips, contentDescription = "Filters") { … }, built on FlowRow with the tokens' horizontal and vertical arrangements. Semantics: a traversal group with contentDescription and collectionInfo (one row, as many columns as members), each member given collectionItemInfo; TalkBack then reads the count. Focus after a removal: the group keeps a FocusRequester per member key and requests the next one when a focused member leaves the composition. TanreiTagGroup(spacing: TanreiTagGroupSpacing.chips, semanticLabel: 'Filters', children: […]), built on Wrap with spacing, runSpacing and crossAxisAlignment: WrapCrossAlignment.center; Wrap follows Directionality. Semantics(container: true, label:) around it; Flutter has no list role for a Wrap, so the members are read in order after the label. Focus after a removal: the group compares its children's keys between builds and requests focus on the next chip's FocusNode. <TagGroup spacing="chips" accessibilityLabel="Filters">{…}</TagGroup>: a View with flexDirection: 'row', flexWrap: 'wrap', alignItems: 'center', columnGap and rowGap from the tokens, accessibilityRole="list" (announced on Android; a labelled container on iOS). It reads its children's keys to find the chip that follows a removed one and focuses it through its ref. Follows the app's direction where an ancestor's is not known.

トークン

トークン 参照先 解決値(ライト)
--tanrei-comp-tag-group-gap-chips sys.space.space100 8px
--tanrei-comp-tag-group-gap-tags sys.space.space100 8px
--tanrei-comp-tag-group-motion-move-duration sys.motion.role.selection.travel 250ms
--tanrei-comp-tag-group-motion-move-easing sys.motion.easing.spatial cubic-bezier(0.2, 0, 0, 1)
--tanrei-comp-tag-group-row-gap-chips sys.space.space200 16px
--tanrei-comp-tag-group-row-gap-tags sys.space.space100 8px

フィクスチャ

基準となる描画パターンです。全プラットフォームが同じ ID でスナップショットを撮り、結果を比較できるようにしています。

生成元 specs/tag-group.spec.json, tokens/comp/tag-group.tokens.json, design/tokens/

ガイドライン

タググループの用途

集まり 使うもの
静的なタグ。記事のカテゴリー、設定が取りうる値、ステータスの並びなど spacing="tags" のタググループ。
それぞれのタグのページへ進むタグ タググループに、href を持つチップを入れます。タグはテキストで、リンクにはなれません。
一覧の上の絞り込み、フィールドの下の候補、ユーザーが入力した項目 チップを入れたタググループ。
1 つのコントロールとして働く絞り込み。一度に 1 つだけ、またはいくつでもオンにでき、Tab 1 回で通り過ぎられる selection を指定し、絞り込みチップを入れたタググループ(後述)。
1 行に収めなければならないトグルボタンの並び ツールバー。
2〜4 個の異なるアクション ボタングループ。
いくつかの中から 1 つを選ぶ ラジオグループ、タブ、セグメントボタンのいずれか。
目に見えるラベル、必須・任意の表示、エラーが必要なコントロール フィールドグループ。
フィールドの中に入力された値 フィールド自身が扱います。コンボボックスは、自分でチップを並べます。

間隔

spacing は、集まりの中身を伝え、2 つの距離を決めます。1 行の中のメンバー同士の距離と、行と行の距離です。

  • chips(既定値)は、チップのための値です。チップは、見えている縁の外側でも、タッチターゲットの大きさまで押下を受け取ります。行と行は、medium のチップがそのターゲットを丸ごと保てるだけ離れます。2 つのチップの領域が重なるところでは、領域は間隔の真ん中で接します。
  • tags は、静的なタグのための値です。行と行は、1 行の中のタグ同士と同じ近さで並ぶので、折り返した集まりが 1 つのまとまりに見えます。

既定値は安全な側です。タグの集まりを chips のままにしても、少し離れて並ぶだけです。チップの集まりに tags を指定しても動作しますが、行と行がタッチターゲットの高さより近くなるので、デバッグビルドがそのことを報告します。

small のチップは、集まりが 1 行に収まる、密度の高い場所のためのものです。折り返した行では、タッチターゲットが本来の大きさになる前に、行の間で接します。タッチ画面で折り返す集まりには、medium のチップを使います。

デザインシステムは、spacing に値を追加できます。

レイアウト

  • メンバーは、指定した順に並びます。グループが並べ替えることも、選択されたチップを前へ出すこともありません。
  • 行に収まらないメンバーは、丸ごと次の行へ進みます。切れることも、縮むことも、隠れることも、スクロールすることもなく、「ほか 3 件」のような表示もありません。表示を減らすには、グループに渡すメンバーを減らし、最後のメンバーを、残りを表示するアクションチップにします。
  • 1 つのメンバーがグループより広い場合、そのメンバーはグループの幅になり、自身のラベルを省略記号で切ります。
  • 集まりは、先頭側の端から始まります。行の末尾や中央に置くには、周りのレイアウトでグループを配置します。
  • メンバーは、すべて同じサイズにします。サイズの違うメンバーは、行の中央にそろいます。
  • メンバーのないグループは、何も描かず、場所も取りません。タグのない記事を特別に扱う必要はありません。

ラベル

label は、支援技術に向けた集まりの名前です。「タグ」「絞り込み」のように付けます。描かれることはありません。目に見える名前が必要な場合は、利用者側の見出しやテキストを集まりの前に置き、そのうえで label も指定します。

選択

既定では、グループは値を持ちません。絞り込みチップは、それぞれが自身の selected を持ち、チップの 1 つずつが Tab で止まる位置になります。

絞り込みが 1 つのコントロールとして働く場合は、selection を指定します。それぞれの絞り込みチップに value を付けると、どのチップがオンかをグループが示します。

  • selection="single":オンにできるチップは多くても 1 つで、value がそれを示します。チップをオンにすると、ほかはオフになります。オンのチップをオフにすると、オンのチップはなくなり、value は null になります。それぞれのチップがトグルだからです。
  • selection="multiple":いくつでもオンにでき、values がそれらを示します。

ユーザーがチップを切り替えると、グループは value か values を設定し、detail.value か detail.values を持つ change を 1 回発火します。チップ自身の change は、グループで止まります。利用者側で value や values を設定すると、チップが切り替わり、何も発火しません。

選択がある場合、集まりで Tab が止まる位置は 1 つです。最後にフォーカスのあったチップ、それがなければ最初のオンのチップ、それもなければ最初のチップです。矢印キーは、右から左のレイアウトでも、指す向きのとおりにチップの間を移動し、Home と End で最初と最後のチップへ移ります。これらのキーは何も切り替えず、フォーカスは最後のチップから最初のチップへ折り返しません。Space で、フォーカスのあるチップを切り替えます。

一度に 1 つの並び順と、いくつもの絞り込み

<tanrei-tag-group id="filters" label="絞り込み" selection="multiple" values="open">
  <tanrei-chip kind="filter" label="未完了" value="open"></tanrei-chip>
  <tanrei-chip kind="filter" label="自分の担当" value="assigned"></tanrei-chip>
  <tanrei-chip kind="filter" label="レビュー待ち" value="review"></tanrei-chip>
</tanrei-tag-group>

<script type="module">
  document.querySelector('#filters').addEventListener('change', (event) => {
    // detail.values:いまオンになっているチップの値です。グループはすでにこれを持っています。
    applyFilters(event.detail.values);
  });
</script>
import { Chip, TagGroup } from '@tanrei/react';

<TagGroup label="並べ替え" selection="single" value={order} onChange={(event) => setOrder(event.detail.value)}>
  <Chip kind="filter" label="新しい順" value="newest" />
  <Chip kind="filter" label="最近更新された順" value="updated" />
  <Chip kind="filter" label="コメントの多い順" value="comments" />
</TagGroup>

選択が扱うのは、絞り込みチップだけです。集まりの中のほかのチップ(「追加」のチップや入力された項目など)にも同じ矢印キーで移り、いつもどおりに働きます。入力された項目は、これまでどおり Backspace か Delete で取り除け、そのときフォーカスを受けたチップが、集まりで Tab が止まる位置になります。

絞り込みチップを取り除いても、グループは value と values をそのままにします。その絞り込みもなくすべき場合は、利用者側で変更します。

答えが常にちょうど 1 つで、選択を解除する方法がない選択には、セグメントボタンかラジオグループを使います。

チップを取り除く

チップは取り除くことを求め、実際に取り除くのは利用者側です。フォーカスがそのチップか、その取り除くアクションにあった場合、グループは、後ろに続くチップへフォーカスを移します。後ろに続くチップがなければ、前のチップへ移します。いくつもの項目を続けて取り除く人は、いた場所からそのまま続けられます。

チップの取り除くアクションは、そのチップに destructiveRemove を付けない限り通常の見た目で、グループはそれを変えません。添付ファイルのように、取り除くと 1 回の操作では戻せないものが失われる場所にだけ付けます。適用中の絞り込みや検索語は、1 回の操作で戻せます。

次の 2 つは、利用者側の役割です。

  • フォーカスを受けられるチップが残っていないときは、集まりが属するコントロールへフォーカスを移します。項目を追加するフィールドや、絞り込みを開くボタンなどです。そうしなければ、フォーカスはどこにもなくなります。
  • グループは、何も読み上げません。取り除いたことを伝える必要がある場合は、ライブリージョンかスナックバーで伝えます。たとえば、絞り込まれる一覧から離れた場所で絞り込みを取り除いた場合です。

推奨

  • 静的なタグの集まりには、spacing="tags" を指定します。
  • 集まりには、ページの言語で label を付けます。
  • 1 つの集まりには、1 種類のメンバーを入れます。入力された項目の後ろに「追加」のチップを 1 つ置く場合は例外です。

非推奨

  • ダイアログやフォームのアクションにタググループを使わない。ボタングループを使います。
  • タグとチップ以外のものを入れない。ほかの内容も並べられますが、デバッグビルドで報告されます。
  • グループからメンバーの見た目を変えない。代わりに、レイヤでグループのトークンを変えます。

アクセシビリティ

ロール
list
ラベル付け
label names the list ("Tags", "Filters"); it is not drawn. Each member is one list item and keeps its own name inside it: a tag its text, a chip its label, an input chip's remove action its removeLabel. A filter chip's state is its own pressed state, also while the group holds the selection: the chip then shows the group's selection. An empty group exposes nothing.
最小ターゲット
見た目のサイズが小さくても、ヒット領域は最小ターゲットサイズを満たします。 minimum 32px · pointer 44px · spacing 8px · touch 48px

キーボード操作

キー 動作
Tab / Shift+Tab While selection is none: moves focus to each chip of the set in turn, in the order given; an input chip is one stop, its remove action is not in the Tab order. While selection is single or multiple: the set is one stop — the chip that last had focus, else the first chip that is on, else the first chip (TGR-20). A set of tags has no stop.
Left Arrow / Right Arrow, Home / End While selection is single or multiple: move focus to the chip drawn before or after (the way the arrow points, in a right-to-left layout too), or to the first or the last, without switching anything; focus does not wrap. Up and Down Arrow do nothing. While selection is none the group handles no key.
Enter, Space, Backspace / Delete The focused chip's own keys, as the Chip says: Space switches a filter chip — and, while selection is single or multiple, changes the group's selection (TGR-19); Backspace or Delete removes an input chip. After a removal, focus is on the chip that followed, or on the one before where none follows (TGR-9).

備考

  • Not interactive by itself: the tag group never takes focus itself, and while selection is none it handles no key (TGR-6), so it has no target of its own to size and minimumTarget holds through its chips, each of which extends its own hit area (CHP-5). What the group owes them is room: gap and row-gap decide how much of each extended hit area a chip keeps beside and between its neighbours (TGR-5).
  • Why a list, for tags and for chips alike: a list tells a person how many members there are and where they are among them, which is what a set of like things needs — four tags on an article, the third of nine filters. It holds static text, links and buttons equally, so one component needs no setting for its semantics.
  • Why not a group: a group says that controls belong together and nothing more. It is what a button group is, whose two to four members are different actions; a tag group's members are of one kind and any number.
  • Why not a list box, also while the group holds a selection: a list box is one control whose options hold the selection (aria-selected), and an option cannot hold a button — an input chip's remove action, or the chip's own button — inside it. React Aria's TagGroup exposes a selectable set as a grid, rows that carry aria-selected and the remove button in a cell; Tanrei's chip is a button whose pressed state already says whether it is on, which is how M3's filter chips and Carbon's selectable tags say it, so a selection keeps the list and each chip its pressed state. One choice among several that is not a filter is a radio group, tabs or a segmented button; a choice from a list is a select.
  • Why one stop with arrow keys only with a selection: while selection is none each chip is its own Tab stop, as every Button of a button group is (CHP-23), and a set of links or of entered items is crossed by Tab. A set that holds a selection is one control to the person who uses it, as a segmented button's segments and Material's chip set are: one stop, the arrow keys inside it (TGR-20), focus not wrapping, as in the chip field (CHF-13). The list stays, so a screen reader still says how many chips there are and where one is among them.
  • While selection is none the tag group does not make filter chips exclusive and holds no value: each filter chip is on or off by itself. With selection single the group puts one on at a time and a chip that is on can be switched off, leaving none, because each chip is a toggle; a choice that must always hold one value is a segmented button or a radio group. With multiple any number are on. The group names its chips by their value, as a segmented button names its segments, and its change carries value or values, as the segmented button's does (TGR-19).
  • After the last member is removed the group is gone and cannot hold focus. The product moves focus to what the set belonged to (the field that adds items, the button that opens the filters); otherwise the platform drops it. The docs page says so with an example.
  • Removing and selecting together: an input chip has no selected state (CHP-11), so in a set that holds a selection the entered items are reached by the arrow keys and removed by Backspace or Delete as before, and the filter chips are switched; the chip that receives focus after a removal becomes the set's stop (TGR-21). A filter chip that the product takes away leaves value and values as they were (TGR-17): the product, which took it away, changes them where it wants the filter gone.
  • A removal is not announced by the group. Where it must be (a filter removed far from the list it changes), the product reports it in a live region or with a snackbar.
  • A status among a set of tags never rests on color alone: each tag says it in words (TAG-11).
  • A small chip is for dense places where a set stands on one line. On wrapped lines small chips are 24 high and 16 apart under the default system, so their touch targets meet between the lines at 40; a set of small chips that wraps on a touch screen wants medium chips.
  • Text size: at 200% members grow and the set wraps onto more lines; nothing is cut by the group (TGR-12).
  • Compose and React Native have no list role for an arbitrary layout: the group is a traversal group whose content description is label and whose collection information gives the count (Compose), or a labelled container (React Native), and each member keeps its own role.

生成元 specs/tag-group.spec.json, tokens/sys/measure.tokens.json

コンポーネントが担うこと

  • 集まりは、label を名前とする 1 つのリストとして伝わります。メンバーは、それぞれがリストの 1 項目です。タグはそのテキストとして、チップは状態を持つボタンまたはリンクとして、入力チップは取り除くアクションを横に伴って読み上げられます。
  • リストは、メンバーがいくつあり、いまその何番目にいるかを伝えます。「リスト、4 項目」のように読み上げられます。
  • メンバーは、指定した順に読み上げられ、その順にフォーカスを受けます。右から左のレイアウトでも同じです。
  • 選択がない場合、チップは 1 つずつがフォーカスが止まる位置です。入力チップも 1 か所で、Tab は取り除くアクションを飛ばし、チップ本体での Backspace か Delete で取り除きます。グループ自身はフォーカスを受けず、キーも処理しません。
  • 選択がある場合、集まりでフォーカスが止まる位置は 1 つです。矢印キー、Home、End でチップの間を移動し、それぞれの絞り込みチップは、押されているかどうかをこれまでどおり伝えます。グループはリストのままなので、スクリーンリーダーは、チップがいくつあるかをこれまでどおり伝えます。
  • フォーカスのあったチップが取り除かれると、フォーカスは次のチップへ移ります。取り除くアクションへ移ることはありません。選択がある場合は、そのチップが集まりでフォーカスが止まる位置になります。
  • 空のグループは、何も伝えません。空のリストも、名前もありません。
  • テキストを 200% にすると、メンバーが大きくなり、より多くの行に折り返します。切れるものはなく、横にスクロールすることもありません。
  • 強制カラーモードでも、グループは何も描きません。メンバーが、自身の縁を描きます。
  • 右から左のレイアウトでは、すべての行が右から始まります。

Web での作り:リストは、グループのシャドウルートにある 1 つの要素で、スロットを囲みます。グループは、メンバーがグループの中にある間、それぞれに role="listitem" を書き込み、メンバーが出ていくときに取り除きます。自身の role を持つメンバーや、それ自体に意味のあるネイティブ要素(リンク、ボタン)は、そのロールを保ち、リストの項目にはなりません。そのような要素は span で包みます。

Web での既知の制約:

  • 要素が定義されるまでの間、メンバーは、間の空白で区切られて順にページに置かれ、間隔もリストもありません。最初の描画からレイアウトをそろえたいページは、次の規則を加えます。

    tanrei-tag-group:not(:defined) {
      display: flex;
      flex-wrap: wrap;
      align-items: center;
      gap: var(--tanrei-comp-tag-group-row-gap-chips) var(--tanrei-comp-tag-group-gap-chips);
    }

利用者側で行うこと

  • グループに label を指定します。Tanrei は言葉を同梱しません。
  • 入力チップには、その項目の名前を含む removeLabel を付けます。
  • 選択がある場合は、それぞれの絞り込みチップに、集まりの中で重複しない value を付けます。value のない絞り込みチップは選択から外れ、デバッグビルドで報告されます。
  • チップが remove を発火したら取り除き、最後の 1 つがなくなったら、利用者側でフォーカスを移します。
  • ステータスは、それぞれのタグの言葉で伝えます。色だけでは伝わりません。