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

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

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

デモ

動画で確認したい場合は、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 を使用できないことです。これにより、ポップアップが視覚的に表示されなくなります。

<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 を切り替えています

ポップアップの表示と非表示は視覚的にわかりますが、スクリーン リーダーには視覚的な手がかり以上のものが必要です。ここでは、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)
})

まとめ

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

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

コミュニティ リミックス