Базовый обзор того, как создать доступный компонент с разделенными кнопками.
В этом посте я хочу поделиться размышлениями о том, как создать разделенную кнопку. Попробуйте демо .
Если вы предпочитаете видео, вот версия этого поста на YouTube:
Обзор
Разделенные кнопки — это кнопки, которые скрывают основную кнопку и список дополнительных кнопок. Они полезны для демонстрации общего действия и вложения второстепенных, менее часто используемых действий до тех пор, пока они не понадобятся. Разделенная кнопка может сыграть решающую роль в том, чтобы сделать загруженный дизайн минималистичным. Расширенная разделенная кнопка может даже запомнить последнее действие пользователя и переместить его на первое место.
В вашем почтовом приложении можно найти общую разделенную кнопку. Основное действие — отправить, но, возможно, вы можете отправить его позже или вместо этого сохранить черновик:
Общая область действий удобна, поскольку пользователю не нужно оглядываться по сторонам. Они знают, что важные действия с электронной почтой содержатся в разделенной кнопке.
Части
Давайте разберем основные части разделенной кнопки, прежде чем обсуждать их общую оркестровку и конечный пользовательский опыт. Здесь используется инструмент проверки доступности VisBug, который помогает показать макрос представления компонента, отображая аспекты HTML, стиль и доступность для каждой основной части.
Контейнер разделенной кнопки верхнего уровня
Компонент самого высокого уровня — это встроенный флексбокс с классом gui-split-button
, содержащим основное действие и .gui-popup-button
.
Основная кнопка действия
Первоначально видимая и фокусируемая <button>
помещается в контейнер с двумя совпадающими угловыми формами для фокуса , наведения и активных взаимодействий, которые отображаются внутри .gui-split-button
.
Кнопка переключения всплывающего окна
Элемент поддержки «всплывающая кнопка» предназначен для активации и ссылки на список второстепенных кнопок. Обратите внимание, что это не <button>
кнопка> и на ней нельзя фокусироваться. Однако это якорь позиционирования для .gui-popup
и хост для :focus-within
используемый для представления всплывающего окна.
Всплывающая карточка
Это плавающая дочерняя карточка по отношению к ее якорю .gui-popup-button
, расположенная абсолютно и семантически оборачающая список кнопок.
Второстепенное действие(я)
Фокусируемая <button>
с размером шрифта немного меньшим, чем у основной кнопки действия, имеет значок и стиль, дополняющий основную кнопку.
Пользовательские свойства
Следующие переменные помогают создать цветовую гармонию и являются центральным местом для изменения значений, используемых во всем компоненте.
@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-haspopup
и aria-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
, так как он отсекает всплывающее окно от визуального присутствия.
<ul>
, заполненный содержимым <li><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">
<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
флексбокс для центрирования значка и привязки списка всплывающих кнопок. Как и основная кнопка, она прозрачна до тех пор, пока не будет наведена на нее курсор или не будет с ней взаимодействовать, а также растянута до заполнения.
.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
, location и 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
представляет собой список кнопок с плавающей карточкой, в котором используются настраиваемые свойства и относительные единицы измерения, которые могут быть немного меньше, интерактивно сопоставляться с основной кнопкой и соответствовать бренду за счет использования цвета. Обратите внимание, что значки менее контрастны, тоньше, а тень имеет оттенок синего. Как и в случае с кнопками, сильный пользовательский интерфейс и 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>
Все значки имеют размер, соответствующий font-size
кнопки, внутри которого они используются, за счет использования модуля ch
в качестве inline-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-block
и padding-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
. Библиотека помогает нам сделать это с помощью параметров element
и target
.
popupButtons.forEach(element =>
rovingIndex({
element,
target: 'button',
}))
Теперь элемент передает фокус целевым дочерним элементам <button>
и обеспечивает стандартную навигацию с помощью клавиш со стрелками для просмотра параметров.
Переключение aria-expanded
Хотя визуально очевидно, что всплывающее окно отображается и скрывается, программе чтения с экрана требуется нечто большее, чем просто визуальные подсказки. Здесь используется JavaScript, чтобы дополнить взаимодействие :focus-within
управляемое CSS, путем переключения соответствующего атрибута программы чтения с экрана.
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)
})
Заключение
Теперь, когда вы знаете, как я это сделал, как бы вы‽ 🙂
Давайте разнообразим наши подходы и изучим все способы разработки в Интернете. Создайте демо, пришлите мне ссылку в Твиттере , и я добавлю ее в раздел ремиксов сообщества ниже!