建立分割按鈕元件

基本介紹如何建構無障礙的分割按鈕元件。

在這篇文章中,我想分享如何建立分割按鈕的想法。立即試用

示範

如果你偏好使用影片,也可以觀看這篇 YouTube 文章:

總覽

「分割按鈕」是隱藏主要按鈕和其他按鈕清單的按鈕。這些 API 有助於公開常見的動作,將常用動作建立巢狀結構,將較不常用的動作建立巢狀結構,直到需要為止。分割按鈕對於忙碌的設計而言非常重要進階分割按鈕甚至可能會記住使用者的最後動作 並將其推廣至主要位置

在電子郵件應用程式中找到常用的分割按鈕。系統會傳送主要動作,但您可以稍後傳送或儲存草稿:

電子郵件應用程式中顯示的分割按鈕範例。

共用動作區域很好,因為使用者不必四處瀏覽。他們知道重要的電子郵件動作包含在分割按鈕中。

零件

我們先詳細瞭解分割按鈕的基本部分,再討論其整體自動化調度管理和最終使用者體驗。這裡使用 VisBug 的無障礙功能檢查工具,以巨集檢視顯示元件,顯示每個主要部分的 HTML、樣式和無障礙設計部分。

組成分割按鈕的 HTML 元素。

頂層分割按鈕容器

最高層級元件是內嵌 Flexbox,其中 gui-split-button 類別包含主要動作.gui-popup-button

系統已檢查 gui-split-button 類別,並顯示這個類別使用的 CSS 屬性。

主要動作按鈕

最初可見且可聚焦的 <button> 會放置在容器中,其中包含用於聚焦hover有效互動的兩個相符邊角形狀,且會出現在 .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-visible,而不是 :focus:focus 是打造無障礙使用者介面的重要途徑,但只會有一項缺點:使用者不一定需要查看該介面,而是套用至所有焦點。

下方影片會嘗試拆解這種微互動,以示範 :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 和使用者體驗也是由這些小細節堆疊顯示。

浮動資訊卡元素。

.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好友將根據文件方向旋轉。- inline-size 而非 width,可確保大小與實際尺寸無關。- border-inline-start 會在開頭加上邊框,這個邊框可能在右側或左側,視指令碼方向而定。

JavaScript

下列 JavaScript 幾乎都是強化無障礙功能。我有兩個輔助程式庫,讓工作變得更輕鬆。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 中的第一個 (或最近聚焦) 按鈕。這個程式庫可協助我們使用 elementtarget 參數執行此操作。

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

結論

現在既然你已經知道我怎麼做,你會怎麼做‽ 🙂?

讓我們帶您更多元的方法,並瞭解運用網路打造網站的所有方式。 請建立示範並透過 Twitter 推文連結,我就能將這項工具新增至下方的「社群重混」部分!

社群重混作品