Webパーツ辞典

スプリットボタンSplit button

よく使う操作のボタンの右に、小さな▼のボタンがくっついた部品。▼を押すと「下書き保存」のような別のやり方が出てくる。

表示例

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

別名・英語表記

  • 分割ボタン
  • ドロップダウン付きボタン
  • メニュー付きボタン
  • 三角付きのボタン
  • プルダウン付きボタン
  • Split button

実装例

<div class="x-split-button">
  <div class="x-split-button__g" role="group" aria-label="保存の操作">
    <button class="x-split-button__main" type="button">保存</button>
    <button class="x-split-button__more" type="button" aria-label="その他の保存方法" aria-haspopup="menu" aria-expanded="true" aria-controls="x-split-button-m">
      <i class="fa-solid fa-chevron-down" aria-hidden="true"></i>
    </button>
  </div>
  <!-- 見本なので開いた状態で流れの中に置いている。実際は ul に popover 属性、▼に popovertarget="x-split-button-m" を付けて開閉し、
       aria-expanded は false から始めて toggle イベントで書き換える(hidden 属性では閉じない)。開いたら最初の項目へフォーカスを移す -->
  <ul class="x-split-button__menu" id="x-split-button-m" role="menu" aria-label="その他の保存方法">
    <li role="none"><button class="x-split-button__item" type="button" role="menuitem" tabindex="-1">下書き保存</button></li>
    <li role="none"><button class="x-split-button__item" type="button" role="menuitem" tabindex="-1">テンプレートとして保存</button></li>
  </ul>
</div>

AIへの指示文

「スプリットボタン(Split button)」を実装してください。

## これは何か
よく使う操作のボタンの右に、小さな▼のボタンがくっついた部品。▼を押すと「下書き保存」のような別のやり方が出てくる。

## 使いどころ
- いつもは「保存」だが、ときどき「下書き保存」「テンプレートとして保存」も使う、のように主な操作と派生の操作があるとき
- 派生の操作のためにボタンを横に並べると場所を取りすぎるとき

## 実装の要件
- 左の主ボタンと右の▼ボタンを隙間なく並べ、境目に1pxの線を入れる。角丸は外側の角だけに付ける
- メニューは▼の下に出す
- 表示例は開いた状態を流れの中に置いている。実運用ではメニューに popover 属性を付けて▼の popovertarget で開閉し、外側クリックと Esc で閉じる動きはブラウザに任せる(Chrome・Edge は 114 から、Safari は 17 から、Firefox は 125 から。iOS の Safari が外側のタップで閉じるのは 18.3 から)
- 項目を選んだときは hidePopover() で閉じる(項目の click は外側クリックではないので、書かないと開いたまま残る)
- ▼の aria-expanded はメニューの toggle イベントの newState(open / closed)に合わせて true / false に書き換え、外側クリックや Esc で閉じたときも古い値を残さない
- 開いたら最初の項目へフォーカスを移すので、▼の click で先に this.focus() を呼ぶ。Safari と macOS の Firefox はマウスで押したボタンにフォーカスを移さず、そのままだと Esc で閉じたときに▼へ戻らない
- 位置は見本の margin も含めて @supports (position-area: block-end) の中だけに書く。popovertarget で開いたメニューは、押した▼が暗黙のアンカーになる(Chrome・Edge は 133 から、Safari は 26 から、Firefox は 147 から)
- @supports の中では margin: 6px 0 0; inset: auto; width: max-content; position-area: block-end span-inline-start; を指定し、▼の下に右端をそろえて出す
- 同じ @supports の中で position-try-fallbacks: flip-block, flip-inline, flip-block flip-inline; を付け、下端では上へ、左端では▼の左端にそろえて右へ回す。width: max-content が無いと、左端で回らずに細く縮む
- position-area に対応しないブラウザ向けに、@supports not (position-area: block-end) の中でメニューに margin: auto; を書き、画面の中央に出す(popover の既定の margin: auto に頼ると、margin を 0 にするリセット CSS に消されて画面の端に張りつく)
- 主な操作と▼の中身が別の種類の操作だと混乱する。▼には主な操作の言い換え・変形だけを入れる
- ドロップダウンメニューはボタン全体が開くだけで、押してすぐ実行される操作を持たない。スプリットボタンは左側を押せばその場で実行される
- ボタングループは並んだボタンが対等で、どれも同じ重さ。スプリットボタンは左が主役で、▼は補助
- 左右の境目が見えないと1つのボタンに見え、▼を押したつもりで保存が走る。仕切りの線と▼の幅(44px目安)で押す場所を分ける

## アクセシビリティの要件
- 左と▼は別々の button にし、別々の名前を付ける(例: 「保存」と「その他の保存方法」)。▼のアイコンは aria-hidden="true"
- ▼には aria-haspopup="menu" と aria-expanded を付け、開閉に合わせて true / false を切り替える
- メニューは role="menu" と role="menuitem" で作り、開いたら最初の項目へフォーカスを移す。上下キーで移動、Esc で閉じて▼へフォーカスを戻す
- 2つを role="group" で包んで aria-label を付けると、同じ操作のまとまりだと伝わる

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

```html
<div class="x-split-button">
  <div class="x-split-button__g" role="group" aria-label="保存の操作">
    <button class="x-split-button__main" type="button">保存</button>
    <button class="x-split-button__more" type="button" aria-label="その他の保存方法" aria-haspopup="menu" aria-expanded="true" aria-controls="x-split-button-m">
      <i class="fa-solid fa-chevron-down" aria-hidden="true"></i>
    </button>
  </div>
  <!-- 見本なので開いた状態で流れの中に置いている。実際は ul に popover 属性、▼に popovertarget="x-split-button-m" を付けて開閉し、
       aria-expanded は false から始めて toggle イベントで書き換える(hidden 属性では閉じない)。開いたら最初の項目へフォーカスを移す -->
  <ul class="x-split-button__menu" id="x-split-button-m" role="menu" aria-label="その他の保存方法">
    <li role="none"><button class="x-split-button__item" type="button" role="menuitem" tabindex="-1">下書き保存</button></li>
    <li role="none"><button class="x-split-button__item" type="button" role="menuitem" tabindex="-1">テンプレートとして保存</button></li>
  </ul>
</div>
```

```css
/* 貼った先のリセットに頼らないよう、この部品の中だけ border-box にする */
.x-split-button,
.x-split-button *:not([class*="fa-"]),
.x-split-button *:not([class*="fa-"])::before,
.x-split-button *: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-split-button {
  width: 220px;
  max-width: 100%;
}
.x-split-button__g {
  display: inline-flex;
}
.x-split-button__main,
.x-split-button__more {
  min-height: 44px;
  border: 1px solid transparent;
  background: #1d4ed8;
  color: #fff;
  font: inherit;
  font-size: 14px;
  cursor: pointer;
}
.x-split-button__main {
  min-width: 88px;
  padding: 0 18px;
  border-radius: 8px 0 0 8px;
  font-weight: 700;
}
/* ▼は別のボタン。1px の白い仕切りで、押す場所が2つあることを見せる */
.x-split-button__more {
  width: 44px;
  border-left: 1px solid #fff;
  border-radius: 0 8px 8px 0;
  font-size: 13px;
}
.x-split-button__main:hover,
.x-split-button__more:hover {
  background: #1b45bd;
}
.x-split-button__more[aria-expanded="true"] {
  background: #1e3a8a;
}
.x-split-button__main:focus-visible,
.x-split-button__more:focus-visible {
  outline: 2px solid #1b2028;
  outline-offset: 2px;
  position: relative;
}
.x-split-button__menu {
  margin: 6px 0 0;
  padding: 4px;
  list-style: none;
  border: 1px solid #7f8a99;
  border-radius: 8px;
  background: #fff;
}
.x-split-button__item {
  display: block;
  width: 100%;
  min-height: 40px;
  padding: 0 12px;
  border: 0;
  border-radius: 6px;
  background: transparent;
  color: #1b2028;
  font: inherit;
  font-size: 14px;
  text-align: left;
  cursor: pointer;
}
.x-split-button__item:hover {
  background: #eef2ff;
}
.x-split-button__item:focus-visible {
  outline: 2px solid #1d4ed8;
  outline-offset: -2px;
}
```

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

使いどころ

  • いつもは「保存」だが、ときどき「下書き保存」「テンプレートとして保存」も使う、のように主な操作と派生の操作があるとき
  • 派生の操作のためにボタンを横に並べると場所を取りすぎるとき

注意点

  • 主な操作と▼の中身が別の種類の操作だと混乱する。▼には主な操作の言い換え・変形だけを入れる
  • ドロップダウンメニューはボタン全体が開くだけで、押してすぐ実行される操作を持たない。スプリットボタンは左側を押せばその場で実行される
  • ボタングループは並んだボタンが対等で、どれも同じ重さ。スプリットボタンは左が主役で、▼は補助
  • 左右の境目が見えないと1つのボタンに見え、▼を押したつもりで保存が走る。仕切りの線と▼の幅(44px目安)で押す場所を分ける

アクセシビリティの要点

  • 左と▼は別々の button にし、別々の名前を付ける(例: 「保存」と「その他の保存方法」)。▼のアイコンは aria-hidden="true"
  • ▼には aria-haspopup="menu" と aria-expanded を付け、開閉に合わせて true / false を切り替える
  • メニューは role="menu" と role="menuitem" で作り、開いたら最初の項目へフォーカスを移す。上下キーで移動、Esc で閉じて▼へフォーカスを戻す
  • 2つを role="group" で包んで aria-label を付けると、同じ操作のまとまりだと伝わる

作るときに使えるツール

関連するパーツ