分割ボタン コンポーネントを作成する

アクセス可能な分割ボタン コンポーネントを作成する方法の基本的な概要。

この投稿では、分割ボタンの作成に関する考え方を共有したいと思います。デモをお試しください

デモ

動画をご覧になる場合は、この投稿の YouTube バージョンをご覧ください。

概要

分割ボタンは、メインのボタンと追加のボタンのリストを非表示にするボタンです。これは、一般的なアクションを公開し、あまり使用されない二次アクションを必要になるまでネストするのに役立ちます。分割ボタンは、煩雑なデザインを最小限に抑えるうえで非常に重要です。高度な分割ボタンでは、最後のユーザー アクションを記憶して、メインの位置にプロモートすることもできます。

メール アプリケーションに共通の分割ボタンがあります。メインの操作は send ですが、後で送信したり、下書きを保存したりすることもできます。

メールアプリケーションに表示される分割ボタンの例。

共有アクション領域は、ユーザーが見回す必要がないため便利です。重要なメール アクションが分割ボタンに含まれていることを認識しています。

部品

分割ボタンの重要な部分を確認してから、全体的なオーケストレーションと最終的なユーザー エクスペリエンスについて説明します。ここで VisBug のユーザー補助検査ツールを使用し、各主要部分の HTML、スタイル、ユーザー補助の側面を明らかにして、コンポーネントのマクロビューを表示します。

分割ボタンを構成する HTML 要素。

最上位の分割ボタンコンテナ

最上位のコンポーネントは、プライマリ アクション.gui-popup-button を含む gui-split-button クラスのインライン Flexbox です。

gui-split-button クラスを検査し、このクラスで使用される CSS プロパティを表示しています。

メイン アクション ボタン

最初に表示されてフォーカス可能な <button> は、フォーカスhoverアクティブのインタラクションの 2 つの一致する角の形状を持つコンテナ内に収まり、.gui-split-button 内に含まれます。

ボタン要素の CSS ルールを表示しているインスペクタ。

ポップアップの切り替えボタン

「ポップアップ ボタン」サポート要素は、セカンダリ ボタンを有効にしてリストを示すためのものです。これは <button> ではなく、フォーカス可能ではないことに注意してください。ただし、.gui-popup の位置指定アンカーであり、ポップアップの表示に使用される :focus-within のホストです。

gui-popup-button クラスの CSS ルールが表示されているインスペクタ

ポップアップ カード

これはアンカー .gui-popup-button に対するフローティング カードの子であり、ボタンリストを絶対的に配置され、意味的にラップします。

gui-popup クラスの CSS ルールが表示されているインスペクタ

サブアクション

プライマリ アクション ボタンよりも少し小さいフォントサイズでフォーカス可能な <button> は、アイコンと、プライマリボタンの補完的なスタイルを備えています。

ボタン要素の CSS ルールを表示しているインスペクタ。

カスタム プロパティ

以下の変数は、色の調和を作り出し、コンポーネント全体で使用される値を一元的に変更するのに役立ちます。

@custom-media --motionOK (prefers-reduced-motion: no-preference);
@custom-media --dark (prefers-color-scheme: dark);
@custom-media --light (prefers-color-scheme: light);

.gui-split-button {
  --theme:             hsl(220 75% 50%);
  --theme-hover:  hsl(220 75% 45%);
  --theme-active:  hsl(220 75% 40%);
  --theme-text:      hsl(220 75% 25%);
  --theme-border: hsl(220 50% 75%);
  --ontheme:         hsl(220 90% 98%);
  --popupbg:         hsl(220 0% 100%);

  --border: 1px solid var(--theme-border);
  --radius: 6px;
  --in-speed: 50ms;
  --out-speed: 300ms;

  @media (--dark) {
    --theme:             hsl(220 50% 60%);
    --theme-hover:  hsl(220 50% 65%);
    --theme-active:  hsl(220 75% 70%);
    --theme-text:      hsl(220 10% 85%);
    --theme-border: hsl(220 20% 70%);
    --ontheme:         hsl(220 90% 5%);
    --popupbg:         hsl(220 10% 30%);
  }
}

レイアウトと色

マークアップ

この要素は、カスタムクラス名を持つ <div> で始まります。

<div class="gui-split-button"></div>

プライマリボタンと .gui-popup-button 要素を追加します。

<div class="gui-split-button">
  <button>Send</button>
  <span class="gui-popup-button" aria-haspopup="true" aria-expanded="false" title="Open for more actions"></span>
</div>

aria 属性の aria-haspopuparia-expanded に注意してください。これらの手がかりは、スクリーン リーダーが分割ボタンのエクスペリエンスの機能と状態を認識するうえで重要です。title 属性はすべての人に役立ちます。

<svg> アイコンと .gui-popup コンテナ要素を追加します。

<div class="gui-split-button">
  <button>Send</button>
  <span class="gui-popup-button" aria-haspopup="true" aria-expanded="false" title="Open for more actions">
    <svg aria-hidden="true" viewBox="0 0 20 20">
      <path d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" />
    </svg>
    <ul class="gui-popup"></ul>
  </span>
</div>

単純なポップアップ プレースメントの場合、.gui-popup は展開するボタンの子になります。この戦略で問題となるのは、.gui-split-button コンテナが overflow: hidden を使用できないことです。これは、ポップアップがクリップされて表示されなくなるためです。

<li><button> のコンテンツが入力された <ul> は、スクリーン リーダーに「ボタンリスト」として読み上げられます。これがまさに提示されるインターフェースです。

<div class="gui-split-button">
  <button>Send</button>
  <span class="gui-popup-button" aria-haspopup="true" aria-expanded="false" title="Open for more actions">
    <svg aria-hidden="true" viewBox="0 0 20 20">
      <path d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" />
    </svg>
    <ul class="gui-popup">
      <li>
        <button>Schedule for later</button>
      </li>
      <li>
        <button>Delete</button>
      </li>
      <li>
        <button>Save draft</button>
      </li>
    </ul>
  </span>
</div>

https://heroicons.com のセカンダリ ボタンに、スタイルを工夫して楽しませるためのアイコンを追加しました。メインとサブのボタンのどちらでも、アイコンはオプションです。

<div class="gui-split-button">
  <button>Send</button>
  <span class="gui-popup-button" aria-haspopup="true" aria-expanded="false" title="Open for more actions">
    <svg aria-hidden="true" viewBox="0 0 20 20">
      <path d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" />
    </svg>
    <ul class="gui-popup">
      <li><button>
        <svg aria-hidden="true" viewBox="0 0 24 24">
          <path d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" />
        </svg>
        Schedule for later
      </button></li>
      <li><button>
        <svg aria-hidden="true" viewBox="0 0 24 24">
          <path d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
        </svg>
        Delete
      </button></li>
      <li><button>
        <svg aria-hidden="true" viewBox="0 0 24 24">
          <path d="M5 5a2 2 0 012-2h10a2 2 0 012 2v16l-7-3.5L5 21V5z" />
        </svg>
        Save draft
      </button></li>
    </ul>
  </span>
</div>

スタイル

HTML とコンテンツを設定すると、スタイルを使用して色とレイアウトを設定できます。

分割ボタンのコンテナのスタイル設定

inline-flex 表示タイプは、他の分割ボタン、アクション、要素とインラインに収まる必要があるため、この折り返しコンポーネントに適しています。

.gui-split-button {
  display: inline-flex;
  border-radius: var(--radius);
  background: var(--theme);
  color: var(--ontheme);
  fill: var(--ontheme);

  touch-action: manipulation;
  user-select: none;
  -webkit-tap-highlight-color: transparent;
}

分割ボタン。

<button> のスタイル設定

ボタンは、必要なコードの量をわかりやすくするのに最適です。ブラウザのデフォルトのスタイルを元に戻すか置き換えなければならない場合もありますが、継承を適用し、操作状態を追加して、さまざまなユーザー設定や入力タイプに適応する必要もあります。ボタンのスタイルはすぐに増えていきます。

これらのボタンは親要素と背景を共有するため、通常のボタンとは異なります。通常、ボタンには背景とテキストの色を設定します。ただし、これらは共有し、インタラクションに独自の背景を適用するだけです。

.gui-split-button button {
  cursor: pointer;
  appearance: none;
  background: none;
  border: none;

  display: inline-flex;
  align-items: center;
  gap: 1ch;
  white-space: nowrap;

  font-family: inherit;
  font-size: inherit;
  font-weight: 500;

  padding-block: 1.25ch;
  padding-inline: 2.5ch;

  color: var(--ontheme);
  outline-color: var(--theme);
  outline-offset: -5px;
}

いくつかの CSS 疑似クラスを使用して操作状態を追加し、その状態に対応するカスタム プロパティを使用します。

.gui-split-button button {
  …

  &:is(:hover, :focus-visible) {
    background: var(--theme-hover);
    color: var(--ontheme);

    & > svg {
      stroke: currentColor;
      fill: none;
    }
  }

  &:active {
    background: var(--theme-active);
  }
}

メインボタンには、デザイン効果を完成させるために、いくつかの特別なスタイルが必要です。

.gui-split-button > button {
  border-end-start-radius: var(--radius);
  border-start-start-radius: var(--radius);

  & > svg {
    fill: none;
    stroke: var(--ontheme);
  }
}

最後に、ライトモードのボタンとアイコンにシャドウを付けます。

.gui-split-button {
  @media (--light) {
    & > button,
    & button:is(:focus-visible, :hover) {
      text-shadow: 0 1px 0 var(--theme-active);
    }
    & > .gui-popup-button > svg,
    & button:is(:focus-visible, :hover) > svg {
      filter: drop-shadow(0 1px 0 var(--theme-active));
    }
  }
}

優れたボタンは、マイクロ インタラクションや細部まで気を配っています。

:focus-visible に関する注意事項

ボタンのスタイルで :focus ではなく :focus-visible が使用されていることに注目してください。:focus は、アクセス可能なユーザー インターフェースを作成するうえで重要な役割を果たしますが、デメリットが 1 つあります。それは、ユーザーがそれを見る必要があるかどうかをインテリジェントではなく、どのフォーカスにも適用できるという点です。

次の動画では、このマイクロインタラクションを分解して、:focus-visible がインテリジェントな代替手段であることを示しています。

ポップアップ ボタンのスタイルを設定する

アイコンを中央に配置し、ポップアップ ボタンのリストを固定するための 4ch Flexbox。メインボタンと同様に、カーソルを合わせるか操作するまで透明であり、拡大されて塗りつぶされます。

ポップアップをトリガーするために使用される分割ボタンの矢印部分。

.gui-popup-button {
  inline-size: 4ch;
  cursor: pointer;
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border-inline-start: var(--border);
  border-start-end-radius: var(--radius);
  border-end-end-radius: var(--radius);
}

CSS ネスト:is() 機能セレクタを使用して、カーソルを合わせた状態、フォーカスした状態、アクティブな状態にレイヤを設定できます。

.gui-popup-button {
  …

  &:is(:hover,:focus-within) {
    background: var(--theme-hover);
  }

  /* fixes iOS trying to be helpful */
  &:focus {
    outline: none;
  }

  &:active {
    background: var(--theme-active);
  }
}

これらのスタイルは、ポップアップの表示と非表示を切り替える主要なフックです。.gui-popup-button の子のいずれかに focus がある場合は、アイコンとポップアップに opacity、位置、pointer-events を設定します。

.gui-popup-button {
  …

  &:focus-within {
    & > svg {
      transition-duration: var(--in-speed);
      transform: rotateZ(.5turn);
    }
    & > .gui-popup {
      transition-duration: var(--in-speed);
      opacity: 1;
      transform: translateY(0);
      pointer-events: auto;
    }
  }
}

インスタイルとアウト スタイルが完了したら、最後の部分として、ユーザーのモーションの好みに応じて、条件付き遷移変換を行います。

.gui-popup-button {
  …

  @media (--motionOK) {
    & > svg {
      transition: transform var(--out-speed) ease;
    }
    & > .gui-popup {
      transform: translateY(5px);

      transition:
        opacity var(--out-speed) ease,
        transform var(--out-speed) ease;
    }
  }
}

コードを細かく確認すると、動きの軽減を好むユーザーにとっては、不透明度が移行されることがわかります。

ポップアップのスタイルを設定する

.gui-popup 要素は、カスタム プロパティと相対単位を使用したフローティング カードボタンのリストで、わずかに小さくなり、メインのボタンとインタラクティブに一致し、色を使用してブランドに合わせます。アイコンはコントラストが低く、薄く、シャドウは濃い青色になっています。ボタンと同様に、優れた UI と UX は、こうした細かいディテールを重ねることで得られます。

フローティング カード要素。

.gui-popup {
  --shadow: 220 70% 15%;
  --shadow-strength: 1%;

  opacity: 0;
  pointer-events: none;

  position: absolute;
  bottom: 80%;
  left: -1.5ch;

  list-style-type: none;
  background: var(--popupbg);
  color: var(--theme-text);
  padding-inline: 0;
  padding-block: .5ch;
  border-radius: var(--radius);
  overflow: hidden;
  display: flex;
  flex-direction: column;
  font-size: .9em;
  transition: opacity var(--out-speed) ease;

  box-shadow:
    0 -2px 5px 0 hsl(var(--shadow) / calc(var(--shadow-strength) + 5%)),
    0 1px 1px -2px hsl(var(--shadow) / calc(var(--shadow-strength) + 10%)),
    0 2px 2px -2px hsl(var(--shadow) / calc(var(--shadow-strength) + 12%)),
    0 5px 5px -2px hsl(var(--shadow) / calc(var(--shadow-strength) + 13%)),
    0 9px 9px -2px hsl(var(--shadow) / calc(var(--shadow-strength) + 14%)),
    0 16px 16px -2px hsl(var(--shadow) / calc(var(--shadow-strength) + 20%))
  ;
}

アイコンとボタンにはブランドカラーが付けられており、ダークテーマとライトテーマのそれぞれのカード内で適切にスタイル設定されます。

購入手続き、クイックペイ、後で購入するために保存のリンクとアイコン。

.gui-popup {
  …

  & svg {
    fill: var(--popupbg);
    stroke: var(--theme);

    @media (prefers-color-scheme: dark) {
      stroke: var(--theme-border);
    }
  }

  & button {
    color: var(--theme-text);
    width: 100%;
  }
}

ダークモードのポップアップには、テキストとアイコンのシャドウが追加され、さらにボックス シャドウがやや濃くなっています。

ダークモードのポップアップ。

.gui-popup {
  …

  @media (--dark) {
    --shadow-strength: 5%;
    --shadow: 220 3% 2%;

    & button:not(:focus-visible, :hover) {
      text-shadow: 0 1px 0 var(--ontheme);
    }

    & button:not(:focus-visible, :hover) > svg {
      filter: drop-shadow(0 1px 0 var(--ontheme));
    }
  }
}

汎用的な <svg> アイコンのスタイル

すべてのアイコンは、ch ユニットを inline-size として使用することで、使用するボタン font-size に相対的なサイズが設定されます。各アイコンには、アイコンの輪郭が柔らかく滑らかになるスタイルも用意されています。

.gui-split-button svg {
  inline-size: 2ch;
  box-sizing: content-box;
  stroke-linecap: round;
  stroke-linejoin: round;
  stroke-width: 2px;
}

右から左へのレイアウト

複雑な処理はすべて論理プロパティで行います。使用される論理プロパティは次のとおりです。 - display: inline-flex は、インライン Flex 要素を作成します。 - padding-blockpadding-inline をペアとして指定すると、省略形の padding ではなく、論理的な両側にパディングができるというメリットがあります。 - border-end-start-radius友だちは、ドキュメントの向きに基づいて角を丸くします。- width ではなく inline-size を使用すると、サイズが物理寸法に関連付けられなくなります。 - border-inline-start は、先頭に枠線を追加します。スクリプトの向きに応じて、右側または左側に表示されます。

JavaScript

以下の JavaScript のほとんどは、ユーザー補助機能を強化するためのものです。タスクを少し簡単にするために、2 つのヘルパー ライブラリを使用しています。BlingBlingJS は、簡潔な DOM クエリと簡単なイベント リスナーの設定に使用され、roving-ux は、ポップアップでアクセス可能なキーボードやゲームパッドの操作を容易にします。

import $ from 'blingblingjs'
import {rovingIndex} from 'roving-ux'

const splitButtons = $('.gui-split-button')
const popupButtons = $('.gui-popup-button')

上記のライブラリをインポートし、要素を選択して変数に保存すると、エクスペリエンスのアップグレードを完了できない関数がいくつかあります。

移動インデックス

キーボードまたはスクリーン リーダーが .gui-popup-button をフォーカスするときは、フォーカスを .gui-popup の最初の(または最後にフォーカスされた)ボタンに転送します。このライブラリは、element パラメータと target パラメータを使用してこの処理を行います。

popupButtons.forEach(element =>
  rovingIndex({
    element,
    target: 'button',
  }))

この要素は、ターゲットの子 <button> にフォーカスを渡し、標準の矢印キーによるナビゲーションでオプションをブラウズできるようになりました。

aria-expanded の切り替え

ポップアップの表示 / 非表示は視覚的にわかりますが、スクリーン リーダーには視覚的な手がかりだけでは不十分です。ここでは JavaScript を使用して、スクリーン リーダーの適切な属性を切り替えることで、CSS による :focus-within の操作を補完します。

popupButtons.on('focusin', e => {
  e.currentTarget.setAttribute('aria-expanded', true)
})

popupButtons.on('focusout', e => {
  e.currentTarget.setAttribute('aria-expanded', false)
})

Escape キーの有効化

ユーザーのフォーカスが意図的にトラップに送られたため、離脱する方法を提供する必要があります。最も一般的な方法は、Escape キーの使用を許可することです。子に対するキーボード イベントはすべてこの親にバブルアップするため、ポップアップ ボタンでのキー操作を監視します。

popupButtons.on('keyup', e => {
  if (e.code === 'Escape')
    e.target.blur()
})

ポップアップ ボタンで Escape キーの押下が検出されると、blur() によりフォーカスが自身から削除されます。

分割ボタンのクリック数

最後に、ユーザーがボタンのクリック、タップ、キーボードの操作を行った場合、アプリは適切なアクションを実行する必要があります。ここでもイベント バブリングを使用しますが、今回は .gui-split-button コンテナで、子ポップアップまたはプライマリ アクションからのボタンクリックをキャッチします。

splitButtons.on('click', event => {
  if (event.target.nodeName !== 'BUTTON') return
  console.info(event.target.innerText)
})

おわりに

私のやり方がわかったところで、どうしたらいいですか? 🙂?

多様なアプローチと、ウェブでの構築方法を学んでいきましょう。 デモを作成してツイートのリンクをお願いします。下のコミュニティ リミックス セクションに追加します。

コミュニティのリミックス