表示例
アイコン: Font Awesome 7 Free(fa-solid / fa-regular)
別名・英語表記
- ケバブメニュー
- ミートボールメニュー
- その他メニュー
- オーバーフローメニュー
- 点々メニュー
- 縦三点ボタン
- Kebab menu
実装例
AIへの指示文
「三点メニュー(Kebab menu)」を実装してください。
## これは何か
一覧の行やカードの右端にある「⋮」や「…」のボタン。押すと、その項目だけに効く名前の変更・共有・削除などの操作が小さく開く。
## 使いどころ
- ファイル一覧やカードのように同じ形の項目が並び、項目ごとの操作を行の中に畳んでおきたいとき
- ツールバーやヘッダーで、入り切らない残りの操作を最後の1つのボタンにまとめたいとき
## 実装の要件
- button[aria-label][aria-haspopup="menu"][aria-expanded][aria-controls][popovertarget] + ul[popover][role="menu"] > li[role="menuitem"][tabindex="-1"] の構成にし、閉じた状態を hidden 属性で持たない
- popover にすると、外側クリックと Esc で閉じる動きと、別の行のメニューを開いたときに前のメニューを閉じる動きをブラウザが持つ(Chrome・Edge は 114 から、Safari は 17 から、Firefox は 125 から)。表の overflow にも切られない
- aria-expanded はメニューの toggle イベントの newState(open / closed)に合わせて true / false に書き換え、外側クリックや Esc で閉じたときも古い値を残さない
- 開いたら先頭の項目へフォーカスを移すので、ボタンの click で先に this.focus() を呼ぶ。Safari と macOS の Firefox はマウスで押したボタンにフォーカスを移さず、そのままだと Esc で閉じたときにボタンへ戻らない
- 見本のメニューの position: absolute; top; right と z-index は流れの中の見本用で、popover にしたら外す。最前面の層で absolute のままだと、@supports not の枝で文書の先頭を基準に置かれる
- 位置は @supports (position-area: block-end) の中だけに書く。popovertarget で開いたメニューは押したボタンが暗黙のアンカーになるので、行ごとにメニューがあっても anchor-scope は要らない(Chrome・Edge は 133 から、Safari は 26 から、Firefox は 147 から)
- @supports の中では margin: 2px 0 0; inset: auto; position-area: block-end span-inline-start; position-try-fallbacks: flip-block; を指定し、ボタンの下に右端をそろえて出し、画面の下端では上へ開く
- position-area に対応しないブラウザ向けに、@supports not (position-area: block-end) の中でメニューに margin: auto; を書き、画面の中央に出す(popover の既定の margin: auto に頼ると、margin を 0 にするリセット CSS に消されて画面の端に張りつく)
- フォーカスがメニューの外へ出たときにも閉じる(Tab で外へ出ても popover は閉じない)
- アイコンは Font Awesome の fa-ellipsis-vertical(⋮)か fa-ellipsis(…)を使い、aria-hidden="true" にする
- ドロップダウンメニュー(overlay/dropdown)の一種で、開くボタンが文字でなく点3つのアイコンのもの。ページの行き先をしまうのはハンバーガーメニュー(navigation/hamburger)で、三点メニューにはその項目への操作を入れる
- 縦の点3つ(⋮)をケバブ、横の点3つ(…)をミートボールと呼び分けることがあるが、役割は同じ。1つのサイトではどちらかにそろえる
- よく使う操作まで中にしまわない。何が入っているか見えないので、主な操作(開く・編集)は行そのものか見えるボタンに出す
- 削除のように取り消せない操作は、区切り線で他から離して最後に置き、押したあとに確認を挟む
## アクセシビリティの要件
- WAI-ARIA APG の Menu Button の形にする。button に aria-haspopup="menu" と aria-expanded を付け、開くメニューに role="menu"、各項目に role="menuitem" を付ける
- aria-label は「見積書.pdf の操作」のように、どの項目の操作かまで含める。どの行も「その他」だと、読み上げでは区別できない(WCAG 4.1.2 名前 (name)・役割 (role) 及び値 (value))
- Enter・Space で開いて先頭の項目へフォーカスを移す。下矢印で開いて先頭へ、上矢印で開いて最後へ移す操作は APG で任意。メニューの中は上下の矢印キーで移る
- Esc で閉じ、開いたボタンへフォーカスを戻す。項目を選んだあともボタンへ戻す(削除で行が消えるときは、隣の行へ移す)
- ボタンは小さくなりがちなので、押せる領域を 24×24px 以上取る(WCAG 2.5.8 ターゲットのサイズ(最低限))
## 参考にする実装
この形を出発点にして、プロジェクトの命名規則・色・余白に合わせて調整してください。
```html
<div class="x-kebab-menu">
<ul class="x-kebab-menu__list" role="list">
<li class="x-kebab-menu__row">
<i class="fa-regular fa-file-lines x-kebab-menu__ic" aria-hidden="true"></i>
<span class="x-kebab-menu__name">議事録_9月.docx</span>
<!-- 名前は「何の操作か」まで含める。どの行も「その他」だと、読み上げでは区別できない -->
<button class="x-kebab-menu__b" type="button" aria-label="議事録_9月.docx の操作" aria-haspopup="menu" aria-expanded="false">
<i class="fa-solid fa-ellipsis-vertical" aria-hidden="true"></i>
</button>
</li>
<li class="x-kebab-menu__row">
<i class="fa-regular fa-file-pdf x-kebab-menu__ic" aria-hidden="true"></i>
<span class="x-kebab-menu__name">見積書.pdf</span>
<span class="x-kebab-menu__w">
<button class="x-kebab-menu__b" type="button" aria-label="見積書.pdf の操作" aria-haspopup="menu" aria-expanded="true" aria-controls="x-kebab-menu-m">
<i class="fa-solid fa-ellipsis-vertical" aria-hidden="true"></i>
</button>
<!-- 見本なので開いた状態で流れの中に置いている。実際は ul に popover 属性、ボタンに popovertarget を付けて開閉し、
aria-expanded は false から始めて toggle イベントで書き換える(hidden 属性では閉じない) -->
<ul class="x-kebab-menu__m" id="x-kebab-menu-m" role="menu" aria-label="見積書.pdf の操作">
<li role="menuitem" tabindex="-1"><i class="fa-solid fa-pen" aria-hidden="true"></i>名前を変更</li>
<li role="menuitem" tabindex="-1"><i class="fa-solid fa-share-nodes" aria-hidden="true"></i>共有</li>
<li role="menuitem" tabindex="-1"><i class="fa-solid fa-download" aria-hidden="true"></i>ダウンロード</li>
<li role="separator" class="x-kebab-menu__sep"></li>
<li role="menuitem" tabindex="-1" class="x-kebab-menu__danger"><i class="fa-regular fa-trash-can" aria-hidden="true"></i>削除</li>
</ul>
</span>
</li>
<li class="x-kebab-menu__row">
<i class="fa-regular fa-image x-kebab-menu__ic" aria-hidden="true"></i>
<span class="x-kebab-menu__name">ロゴ案.png</span>
<button class="x-kebab-menu__b" type="button" aria-label="ロゴ案.png の操作" aria-haspopup="menu" aria-expanded="false">
<i class="fa-solid fa-ellipsis-vertical" aria-hidden="true"></i>
</button>
</li>
</ul>
</div>
```
```css
/* 貼った先のリセットに頼らないよう、この部品の中だけ border-box にする */
.x-kebab-menu,
.x-kebab-menu *:not([class*="fa-"]),
.x-kebab-menu *:not([class*="fa-"])::before,
.x-kebab-menu *: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-kebab-menu {
width: 300px;
max-width: 100%;
min-height: 210px;
color: #1b2028;
font-size: 13px;
}
.x-kebab-menu__list {
margin: 0;
padding: 0;
border: 1px solid #7f8a99;
border-radius: 8px;
background: #fff;
list-style: none;
}
.x-kebab-menu__row {
display: flex;
align-items: center;
gap: 10px;
min-height: 44px;
padding: 0 6px 0 12px;
}
.x-kebab-menu__row + .x-kebab-menu__row {
border-top: 1px solid #e3e6ea;
}
.x-kebab-menu__ic {
width: 16px;
color: #5b6472;
text-align: center;
}
.x-kebab-menu__name {
flex: 1;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
/* メニューの位置の基準。ボタンの右端にそろえて下へ開く */
.x-kebab-menu__w {
position: relative;
display: inline-flex;
}
.x-kebab-menu__b {
display: grid;
place-items: center;
width: 32px;
height: 32px;
padding: 0;
border: 0;
border-radius: 50%;
background: none;
color: #1b2028;
font-size: 15px;
cursor: pointer;
}
.x-kebab-menu__b:hover,
.x-kebab-menu__b[aria-expanded="true"] {
background: #e3e6ea;
}
.x-kebab-menu__b:focus-visible,
.x-kebab-menu__m [role="menuitem"]:focus-visible {
outline: 2px solid #1d4ed8;
outline-offset: -2px;
}
/* position・top・right・z-index は流れの中の見本用。popover にしたら外し、位置はアンカー配置で決める */
.x-kebab-menu__m {
position: absolute;
top: calc(100% + 2px);
right: 0;
z-index: 10;
width: 168px;
margin: 0;
padding: 4px;
border: 1px solid #7f8a99;
border-radius: 8px;
background: #fff;
box-shadow: 0 6px 14px rgba(27, 32, 40, 0.14);
list-style: none;
}
.x-kebab-menu__m [role="menuitem"] {
display: flex;
align-items: center;
gap: 10px;
min-height: 30px;
padding: 0 10px;
border-radius: 5px;
font-size: 12.5px;
cursor: pointer;
}
.x-kebab-menu__m [role="menuitem"] i {
width: 14px;
color: #5b6472;
text-align: center;
}
.x-kebab-menu__m [role="menuitem"]:hover {
background: #f1f3f6;
}
.x-kebab-menu__sep {
height: 1px;
margin: 4px 6px;
background: #e3e6ea;
}
/* 取り消せない操作は赤。色だけに頼らず、区切り線で他の操作から離して置く */
.x-kebab-menu__m .x-kebab-menu__danger,
.x-kebab-menu__m .x-kebab-menu__danger i {
color: #b91c1c;
}
```
## 出力の約束
- HTML と CSS を分けて出す。JavaScript が要るときは素の JavaScript で書き、UIフレームワークは使わない。
- 参考実装がアイコンに Font Awesome のクラスを使っている場合は、プロジェクトで使っているアイコンかインライン SVG に置き換えてよい。
- 上のアクセシビリティ要件を満たす。満たせない箇所があるときは、理由を添えて先に知らせる。
- 色・角丸・余白は既存のデザイントークンがあればそれを使う。無ければ CSS 変数として切り出す。
使いどころ
- ファイル一覧やカードのように同じ形の項目が並び、項目ごとの操作を行の中に畳んでおきたいとき
- ツールバーやヘッダーで、入り切らない残りの操作を最後の1つのボタンにまとめたいとき
注意点
- ドロップダウンメニュー(overlay/dropdown)の一種で、開くボタンが文字でなく点3つのアイコンのもの。ページの行き先をしまうのはハンバーガーメニュー(navigation/hamburger)で、三点メニューにはその項目への操作を入れる
- 縦の点3つ(⋮)をケバブ、横の点3つ(…)をミートボールと呼び分けることがあるが、役割は同じ。1つのサイトではどちらかにそろえる
- よく使う操作まで中にしまわない。何が入っているか見えないので、主な操作(開く・編集)は行そのものか見えるボタンに出す
- 削除のように取り消せない操作は、区切り線で他から離して最後に置き、押したあとに確認を挟む
アクセシビリティの要点
- WAI-ARIA APG の Menu Button の形にする。button に aria-haspopup="menu" と aria-expanded を付け、開くメニューに role="menu"、各項目に role="menuitem" を付ける
- aria-label は「見積書.pdf の操作」のように、どの項目の操作かまで含める。どの行も「その他」だと、読み上げでは区別できない(WCAG 4.1.2 名前 (name)・役割 (role) 及び値 (value))
- Enter・Space で開いて先頭の項目へフォーカスを移す。下矢印で開いて先頭へ、上矢印で開いて最後へ移す操作は APG で任意。メニューの中は上下の矢印キーで移る
- Esc で閉じ、開いたボタンへフォーカスを戻す。項目を選んだあともボタンへ戻す(削除で行が消えるときは、隣の行へ移す)
- ボタンは小さくなりがちなので、押せる領域を 24×24px 以上取る(WCAG 2.5.8 ターゲットのサイズ(最低限))