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

アクセシビリティ対応の分割ボタン コンポーネントを構築する方法の基本的な概要。

この記事では、分割ボタンを作成する方法について考えてみたいと思います。デモをお試しください

デモ

動画でご覧になりたい場合は、こちらの YouTube 版をご覧ください。

概要

分割ボタンは、メインボタンと追加ボタンのリストを隠すボタンです。これらは、共通のアクションを公開しつつ、必要になるまで使用頻度の低い二次的なアクションをネストする場合に便利です。分割ボタンは、忙しいデザインを最小限に抑えるために不可欠です。高度な分割ボタンでは、ユーザーの最後のアクションを記憶して、プライマリ ポジションに昇格させることもできます。

一般的な分割ボタンは、メール アプリケーションにあります。主なアクションは送信ですが、後で送信したり、下書きを保存したりすることもできます。

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

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

部品

スプリット ボタンの全体的なオーケストレーションと最終的なユーザー エクスペリエンスについて説明する前に、スプリット ボタンの重要な部分を分解してみましょう。ここでは、VisBug のアクセシビリティ検査ツールを使用して、コンポーネントのマクロビューを表示し、各主要部分の HTML、スタイル、アクセシビリティの側面を明らかにします。

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

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

最上位のコンポーネントは、gui-split-button クラスのインライン フレックスボックスで、プライマリ アクション.gui-popup-button が含まれます。

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

メイン アクション ボタン

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

ボタン要素の 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 を使用できないことです。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 フレックスボックス。メインボタンと同様に、マウスオーバーまたは操作が行われるまで透明で、塗りつぶしのために拡大されます。

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

.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> アイコンのスタイル

すべてのアイコンは、inline-size として ch 単位を使用することで、使用されるボタン 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 はインライン フレックス要素を作成します。- padding の短縮形ではなく、padding-blockpadding-inline をペアで使用すると、論理的な両側にパディングを適用するメリットがあります。- border-end-start-radiusfriends は、ドキュメントの方向に基づいて角を丸めます。- 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)
})

まとめ

私がどのように行ったかをご理解いただけたかと思います。では、あなたならどのようにしますか?🙂

アプローチを多様化し、ウェブで構築するさまざまな方法を学びましょう。デモを作成して、ツイートでリンクを送信してください。下のコミュニティ リミックス セクションに追加します。

コミュニティ リミックス