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

ユーザー補助対応の分割ボタン コンポーネントを作成する方法の基本的な概要。

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

デモ

動画で確認したい場合は、YouTube 版の投稿をご覧ください。

概要

分割ボタンは、メインボタンと追加ボタンのリストを非表示にするボタンです。これらのアクションは、使用頻度の低いセカンダリ アクションを必要に応じてネストしながら、一般的なアクションを公開する場合に便利です。分割ボタンは、複雑なデザインをミニマルに見せるのに役立ちます。高度な分割ボタンでは、ユーザーの最後の操作を記憶して、その操作をプライマリ ポジションに昇格させることもできます。

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

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

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

部品

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

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

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

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

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

メイン アクション ボタン

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

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

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

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

クラス gui-popup-button の CSS ルールを示すインスペクタ。

ポップアップ カード

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

クラス gui-popup の CSS ルールを示すインスペクタ

サブアクション

メイン アクション ボタンよりもフォントサイズが若干小さいフォーカス可能な <button> には、メインボタンと同じアイコンとスタイルが使用されます。

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

カスタム プロパティ

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

@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-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 の切り替え

ポップアップが表示されたり非表示になったりすることは視覚的にわかりますが、スクリーン リーダーには視覚的な手がかり以上のものが必要です。ここでは、スクリーン リーダーに適した属性を切り替えることで、CSS 駆動の :focus-within 操作を補完するために JavaScript が使用されています。

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)
})

まとめ

私の方法をご覧になったところで、あなたならどうしますか?

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

コミュニティ リミックス