Базовый обзор того, как создать гибкий и доступный компонент коммутатора.
В этом посте я хочу поделиться мыслями о том, как создавать компоненты коммутатора. Попробуйте демо .
Если вы предпочитаете видео, вот версия этого поста на YouTube:
Обзор
Переключатель функционирует аналогично флажку, но явно представляет логические состояния включения и выключения.
В этой демонстрации используется <input type="checkbox" role="switch">
для большей части своих функций, преимуществом которой является отсутствие необходимости использования CSS или JavaScript для полной функциональности и доступности. Загрузка CSS обеспечивает поддержку языков с письмом справа налево, вертикальности, анимации и многого другого. Загрузка JavaScript делает переключатель перетаскиваемым и ощутимым.
Пользовательские свойства
Следующие переменные представляют различные части переключателя и их параметры. Как класс верхнего уровня, .gui-switch
содержит пользовательские свойства, используемые во всех дочерних компонентах, и точки входа для централизованной настройки.
Отслеживать
Длина ( --track-size
), отступы и два цвета:
.gui-switch {
--track-size: calc(var(--thumb-size) * 2);
--track-padding: 2px;
--track-inactive: hsl(80 0% 80%);
--track-active: hsl(80 60% 45%);
--track-color-inactive: var(--track-inactive);
--track-color-active: var(--track-active);
@media (prefers-color-scheme: dark) {
--track-inactive: hsl(80 0% 35%);
--track-active: hsl(80 60% 60%);
}
}
Большой палец
Размер, цвет фона и цвета выделения взаимодействия:
.gui-switch {
--thumb-size: 2rem;
--thumb: hsl(0 0% 100%);
--thumb-highlight: hsl(0 0% 0% / 25%);
--thumb-color: var(--thumb);
--thumb-color-highlight: var(--thumb-highlight);
@media (prefers-color-scheme: dark) {
--thumb: hsl(0 0% 5%);
--thumb-highlight: hsl(0 0% 100% / 25%);
}
}
Уменьшение движения
Чтобы добавить четкий псевдоним и уменьшить повторение, пользовательский медиа-запрос с уменьшенными предпочтениями движения можно поместить в пользовательское свойство с помощью плагина PostCSS на основе этого проекта спецификации в Media Queries 5 :
@custom-media --motionOK (prefers-reduced-motion: no-preference);
Разметка
Я решил обернуть свой элемент <input type="checkbox" role="switch">
тегом <label>
, объединив их отношения, чтобы избежать двусмысленности ассоциации флажков и меток, в то же время предоставляя пользователю возможность взаимодействовать с меткой для переключения ввода.
<label for="switch" class="gui-switch">
Label text
<input type="checkbox" role="switch" id="switch">
</label>
<input type="checkbox">
поставляется с предварительно встроенным API и состоянием . Браузер управляет checked
свойством и событиями ввода, такими как oninput
и onchanged
.
Макеты
Flexbox , сетка и пользовательские свойства имеют решающее значение для поддержания стилей этого компонента. Они централизуют значения, дают имена неоднозначным вычислениям или областям и включают небольшой API настраиваемых свойств для простой настройки компонентов.
.gui-switch
Макет верхнего уровня для переключателя — flexbox. Класс .gui-switch
содержит частные и общедоступные пользовательские свойства, которые дети используют для расчета своих макетов.
.gui-switch {
display: flex;
align-items: center;
gap: 2ch;
justify-content: space-between;
}
Расширение и изменение макета флексбокса похоже на изменение любого макета флексбокса. Например, чтобы разместить метки над или под переключателем или изменить flex-direction
:
<label for="light-switch" class="gui-switch" style="flex-direction: column">
Default
<input type="checkbox" role="switch" id="light-switch">
</label>
Отслеживать
Ввод флажка стилизован под дорожку переключения путем удаления его обычного appearance: checkbox
и указания вместо него собственного размера:
.gui-switch > input {
appearance: none;
inline-size: var(--track-size);
block-size: var(--thumb-size);
padding: var(--track-padding);
flex-shrink: 0;
display: grid;
align-items: center;
grid: [track] 1fr / [track] 1fr;
}
Дорожка также создает область дорожки сетки по одной ячейке, на которую может претендовать большой палец.
Большой палец
Стиль « appearance: none
также удаляет визуальную галочку, предоставленную браузером. Этот компонент использует псевдоэлемент и псевдокласс :checked
на входе, чтобы заменить этот визуальный индикатор.
Большой палец — это дочерний псевдоэлемент, прикрепленный к input[type="checkbox"]
и располагающийся поверх дорожки, а не под ней, заявляя область сетки track
:
.gui-switch > input::before {
content: "";
grid-area: track;
inline-size: var(--thumb-size);
block-size: var(--thumb-size);
}
Стили
Пользовательские свойства позволяют использовать универсальный компонент переключателя, который адаптируется к цветовым схемам, языкам с письмом справа налево и предпочтениям движения.
Стили сенсорного взаимодействия
На мобильных устройствах браузеры добавляют функцию выделения касаний и выделения текста к меткам и полям ввода. Это негативно повлияло на стиль и обратную связь по визуальному взаимодействию, необходимые этому переключателю. С помощью нескольких строк CSS я могу удалить эти эффекты и добавить свой собственный стиль cursor: pointer
:
.gui-switch {
cursor: pointer;
user-select: none;
-webkit-tap-highlight-color: transparent;
}
Не всегда целесообразно удалять эти стили, поскольку они могут быть ценной обратной связью при визуальном взаимодействии. Обязательно предоставьте собственные альтернативы, если вы их удалите.
Отслеживать
Стили этого элемента в основном связаны с его формой и цветом, к которым он обращается из родительского .gui-switch
через cascade .
.gui-switch > input {
appearance: none;
border: none;
outline-offset: 5px;
box-sizing: content-box;
padding: var(--track-padding);
background: var(--track-color-inactive);
inline-size: var(--track-size);
block-size: var(--thumb-size);
border-radius: var(--track-size);
}
Широкий выбор вариантов настройки трека переключения можно получить с помощью четырех пользовательских свойств. border: none
добавляется с момента appearance: none
не удаляет границы флажка во всех браузерах.
Большой палец
Элемент большого пальца уже находится на правильном track
, но ему нужны стили круга:
.gui-switch > input::before {
background: var(--thumb-color);
border-radius: 50%;
}
Взаимодействие
Используйте пользовательские свойства, чтобы подготовиться к взаимодействиям, которые будут отображать выделение при наведении и изменение положения большого пальца. Предпочтения пользователя также проверяются перед переходом к стилям выделения при движении или наведении.
.gui-switch > input::before {
box-shadow: 0 0 0 var(--highlight-size) var(--thumb-color-highlight);
@media (--motionOK) { & {
transition:
transform var(--thumb-transition-duration) ease,
box-shadow .25s ease;
}}
}
Положение большого пальца
Пользовательские свойства предоставляют единый механизм размещения ползунка на дорожке. В нашем распоряжении размеры дорожки и ползунка, которые мы будем использовать в расчетах, чтобы обеспечить правильное смещение ползунка внутри дорожки: 0%
и 100%
.
Элемент input
владеет переменной позиции --thumb-position
, а псевдоэлемент большого пальца использует ее как позицию translateX
:
.gui-switch > input {
--thumb-position: 0%;
}
.gui-switch > input::before {
transform: translateX(var(--thumb-position));
}
Теперь мы можем изменить --thumb-position
из CSS и псевдоклассов, предоставляемых в элементах флажков. Поскольку ранее мы условно установили transition: transform var(--thumb-transition-duration) ease
ранее для этого элемента, эти изменения могут анимироваться при изменении:
/* positioned at the end of the track: track length - 100% (thumb width) */
.gui-switch > input:checked {
--thumb-position: calc(var(--track-size) - 100%);
}
/* positioned in the center of the track: half the track - half the thumb */
.gui-switch > input:indeterminate {
--thumb-position: calc(
(var(--track-size) / 2) - (var(--thumb-size) / 2)
);
}
Я думаю, что эта разделенная оркестровка сработала хорошо. Элемент «thumb» связан только с одним стилем — позицией translateX
. Вход может управлять всеми сложностями и вычислениями.
Вертикальный
Поддержка осуществлялась с помощью класса модификатора -vertical
, который добавляет вращение с помощью CSS-преобразований к input
элементу.
Однако 3D-повернутый элемент не меняет общую высоту компонента, что может нарушить компоновку блока. Учитывайте это, используя переменные --track-size
и --track-padding
. Рассчитайте минимальное количество места, необходимое для размещения вертикальной кнопки в макете, как ожидалось:
.gui-switch.-vertical {
min-block-size: calc(var(--track-size) + calc(var(--track-padding) * 2));
& > input {
transform: rotate(-90deg);
}
}
(RTL) справа налево
Мой друг по CSS, Элад Шектер , и я вместе разработали прототип выдвижного бокового меню, используя преобразования CSS, которые обрабатывали языки с письмом справа налево путем переворачивания одной переменной. Мы сделали это потому, что в CSS нет логических преобразований свойств и, возможно, никогда не будет. У Элада возникла замечательная идея использовать значение пользовательского свойства для инвертирования процентов, чтобы обеспечить возможность управления одним расположением нашей собственной пользовательской логики для логических преобразований. Я использовал ту же самую технику в этом переключателе и думаю, что она сработала отлично:
.gui-switch {
--isLTR: 1;
&:dir(rtl) {
--isLTR: -1;
}
}
Пользовательское свойство --isLTR
изначально имеет значение 1
, что означает, что оно true
поскольку по умолчанию наш макет расположен слева направо. Затем, используя псевдокласс CSS :dir()
, значение устанавливается равным -1
, когда компонент находится в макете справа налево.
Примените --isLTR
к действию, используя его в функции calc()
внутри преобразования:
.gui-switch.-vertical > input {
transform: rotate(-90deg);
transform: rotate(calc(90deg * var(--isLTR) * -1));
}
Теперь вращение вертикального переключателя учитывает положение противоположной стороны, необходимое для расположения справа налево.
Преобразования translateX
в псевдоэлементе большого пальца также необходимо обновить, чтобы учесть требования противоположной стороны:
.gui-switch > input:checked {
--thumb-position: calc(var(--track-size) - 100%);
--thumb-position: calc((var(--track-size) - 100%) * var(--isLTR));
}
.gui-switch > input:indeterminate {
--thumb-position: calc(
(var(--track-size) / 2) - (var(--thumb-size) / 2)
);
--thumb-position: calc(
((var(--track-size) / 2) - (var(--thumb-size) / 2))
* var(--isLTR)
);
}
Хотя этот подход не сможет удовлетворить все потребности, связанные с такой концепцией, как логические преобразования CSS, он предлагает некоторые принципы DRY для многих случаев использования.
Штаты
Использование встроенного input[type="checkbox"]
не было бы полным без обработки различных состояний, в которых он может находиться: :checked
, :disabled
, :indeterminate
и :hover
. :focus
намеренно оставлен в покое, скорректировано только его смещение; кольцо фокусировки отлично смотрелось в Firefox и Safari:
Проверено
<label for="switch-checked" class="gui-switch">
Default
<input type="checkbox" role="switch" id="switch-checked" checked="true">
</label>
Это состояние представляет собой on
состояние. В этом состоянии фон входного «дорожки» установлен на активный цвет, а положение ползунка установлено на «конец».
.gui-switch > input:checked {
background: var(--track-color-active);
--thumb-position: calc((var(--track-size) - 100%) * var(--isLTR));
}
Неполноценный
<label for="switch-disabled" class="gui-switch">
Default
<input type="checkbox" role="switch" id="switch-disabled" disabled="true">
</label>
Кнопка :disabled
не только визуально выглядит по-другому, но также должна сделать элемент неизменяемым. Неизменяемость взаимодействия недоступна в браузере, но визуальные состояния нуждаются в стилях из-за использования appearance: none
.
.gui-switch > input:disabled {
cursor: not-allowed;
--thumb-color: transparent;
&::before {
cursor: not-allowed;
box-shadow: inset 0 0 0 2px hsl(0 0% 100% / 50%);
@media (prefers-color-scheme: dark) { & {
box-shadow: inset 0 0 0 2px hsl(0 0% 0% / 50%);
}}
}
}
Это состояние сложное, поскольку для него нужны темные и светлые темы как с отключенным, так и с отмеченным состояниями. Я стилистически выбрал минимальные стили для этих состояний, чтобы облегчить бремя поддержки комбинаций стилей.
Неопределенный
Часто забываемое состояние — :indeterminate
, когда флажок не установлен и не снят. Это веселое состояние, оно привлекательное и скромное. Хорошее напоминание о том, что логические состояния могут иметь скрытое промежуточное положение между состояниями.
Установить неопределенный флажок сложно, его может установить только JavaScript:
<label for="switch-indeterminate" class="gui-switch">
Indeterminate
<input type="checkbox" role="switch" id="switch-indeterminate">
<script>document.getElementById('switch-indeterminate').indeterminate = true</script>
</label>
Поскольку для меня это состояние непритязательное и привлекательное, мне показалось уместным расположить большой палец переключателя посередине:
.gui-switch > input:indeterminate {
--thumb-position: calc(
calc(calc(var(--track-size) / 2) - calc(var(--thumb-size) / 2))
* var(--isLTR)
);
}
Наведите указатель мыши
Взаимодействия при наведении должны обеспечивать визуальную поддержку подключенного пользовательского интерфейса, а также указывать направление к интерактивному пользовательскому интерфейсу. Этот переключатель выделяет большой палец полупрозрачным кольцом при наведении курсора на метку или ввод. Затем эта анимация при наведении указывает направление к интерактивному элементу большого пальца.
Эффект «подсветки» достигается с помощью box-shadow
. При наведении неотключенного ввода увеличьте размер --highlight-size
. Если пользователя устраивает движение, мы перемещаем box-shadow
и видим, как она увеличивается, если его не устраивает движение, подсветка появляется мгновенно:
.gui-switch > input::before {
box-shadow: 0 0 0 var(--highlight-size) var(--thumb-color-highlight);
@media (--motionOK) { & {
transition:
transform var(--thumb-transition-duration) ease,
box-shadow .25s ease;
}}
}
.gui-switch > input:not(:disabled):hover::before {
--highlight-size: .5rem;
}
JavaScript
На мой взгляд, интерфейс коммутатора может показаться странным в своей попытке имитировать физический интерфейс, особенно такой, с кругом внутри дорожки. iOS сделала это правильно благодаря своему переключателю: вы можете перетаскивать их из стороны в сторону, и очень приятно иметь такую возможность. И наоборот, элемент пользовательского интерфейса может считаться неактивным, если при попытке перетаскивания ничего не происходит.
Перетаскиваемые большие пальцы
Псевдоэлемент большого пальца получает свою позицию из области .gui-switch > input
var(--thumb-position)
. JavaScript может предоставить значение встроенного стиля на входе, чтобы динамически обновлять положение большого пальца, создавая впечатление, что он следует за жестом указателя. Когда указатель отпущен, удалите встроенные стили и определите, было ли перетаскивание ближе к выключению или включению, используя пользовательское свойство --thumb-position
. Это основа решения; события указателя, условно отслеживающие позиции указателя для изменения пользовательских свойств CSS.
Поскольку до появления этого сценария компонент уже был на 100% функциональным, для поддержания существующего поведения требуется немало усилий, например, нажатие на метку для переключения ввода. Наш JavaScript не должен добавлять функции за счет существующих функций.
touch-action
Перетаскивание — это жест, настраиваемый, что делает его отличным кандидатом на преимущества touch-action
. В случае с этим переключателем наш скрипт должен обрабатывать горизонтальный жест или вертикальный жест, зафиксированный для варианта вертикального переключателя. С помощью touch-action
мы можем указать браузеру, какие жесты обрабатывать на этом элементе, чтобы скрипт мог обрабатывать жест без конкуренции.
Следующий CSS указывает браузеру, что, когда жест указателя начинается с этой дорожки переключения, обрабатывайте вертикальные жесты и ничего не делайте с горизонтальными:
.gui-switch > input {
touch-action: pan-y;
}
Желаемый результат — горизонтальный жест, который не будет также панорамировать или прокручивать страницу. Указатель может прокручивать по вертикали, начиная с входных данных, и прокручивать страницу, но горизонтальные указатель обрабатываются пользователем.
Утилиты для создания стилей значений пикселей
При настройке и во время перетаскивания из элементов необходимо будет получить различные вычисленные числовые значения. Следующие функции JavaScript возвращают вычисленные значения пикселей с учетом свойства CSS. Он используется в сценарии установки, например getStyle(checkbox, 'padding-left')
.
const getStyle = (element, prop) => {
return parseInt(window.getComputedStyle(element).getPropertyValue(prop));
}
const getPseudoStyle = (element, prop) => {
return parseInt(window.getComputedStyle(element, ':before').getPropertyValue(prop));
}
export {
getStyle,
getPseudoStyle,
}
Обратите внимание, как window.getComputedStyle()
принимает второй аргумент — целевой псевдоэлемент. Довольно здорово, что JavaScript может считывать так много значений из элементов, даже из псевдоэлементов.
dragging
Это основной момент для логики перетаскивания, и есть несколько моментов, на которые следует обратить внимание в обработчике событий функции:
const dragging = event => {
if (!state.activethumb) return
let {thumbsize, bounds, padding} = switches.get(state.activethumb.parentElement)
let directionality = getStyle(state.activethumb, '--isLTR')
let track = (directionality === -1)
? (state.activethumb.clientWidth * -1) + thumbsize + padding
: 0
let pos = Math.round(event.offsetX - thumbsize / 2)
if (pos < bounds.lower) pos = 0
if (pos > bounds.upper) pos = bounds.upper
state.activethumb.style.setProperty('--thumb-position', `${track + pos}px`)
}
Герой скрипта — state.activethumb
, маленький кружок, который этот скрипт позиционирует вместе с указателем. Объект switches
— это Map()
, где ключи — это .gui-switch
, а значения — это кэшированные границы и размеры, которые обеспечивают эффективность сценария. Направление справа налево обрабатывается с использованием того же пользовательского свойства, что и CSS — --isLTR
, и может использовать его для инвертирования логики и продолжения поддержки RTL. event.offsetX
также полезен, поскольку содержит значение дельты, полезное для позиционирования ползунка.
state.activethumb.style.setProperty('--thumb-position', `${track + pos}px`)
Эта последняя строка CSS устанавливает пользовательское свойство, используемое элементом большого пальца. В противном случае это присвоение значения будет меняться со временем, но предыдущее событие указателя временно установило --thumb-transition-duration
в 0s
, устраняя то, что было бы вялым взаимодействием.
dragEnd
Чтобы пользователю было разрешено перетащить далеко за пределы переключателя и отпустить его, необходимо зарегистрировать глобальное событие окна:
window.addEventListener('pointerup', event => {
if (!state.activethumb) return
dragEnd(event)
})
Я думаю, что очень важно, чтобы у пользователя была свобода перетаскивания, и чтобы интерфейс был достаточно умным, чтобы это учитывать. С этим переключателем справиться с этой проблемой не составило особого труда, но в процессе разработки это потребовало тщательного рассмотрения.
const dragEnd = event => {
if (!state.activethumb) return
state.activethumb.checked = determineChecked()
if (state.activethumb.indeterminate)
state.activethumb.indeterminate = false
state.activethumb.style.removeProperty('--thumb-transition-duration')
state.activethumb.style.removeProperty('--thumb-position')
state.activethumb.removeEventListener('pointermove', dragging)
state.activethumb = null
padRelease()
}
Взаимодействие с элементом завершено, пришло время установить свойство ввода проверено и удалить все события жестов. Флажок изменяется с помощью state.activethumb.checked = determineChecked()
.
determineChecked()
Эта функция, вызываемая функцией dragEnd
, определяет, где ток ползунка находится в пределах его дорожки, и возвращает true, если он равен или превышает половину длины дорожки:
const determineChecked = () => {
let {bounds} = switches.get(state.activethumb.parentElement)
let curpos =
Math.abs(
parseInt(
state.activethumb.style.getPropertyValue('--thumb-position')))
if (!curpos) {
curpos = state.activethumb.checked
? bounds.lower
: bounds.upper
}
return curpos >= bounds.middle
}
Дополнительные мысли
Жест перетаскивания вызвал некоторую задолженность по коду из-за выбранной исходной структуры HTML, в основном заключающей в себя вводимые данные в метку. Метка, являющаяся родительским элементом, будет получать взаимодействия с щелчками после ввода. В конце события dragEnd
вы, возможно, заметили, что функция padRelease()
звучит странно.
const padRelease = () => {
state.recentlyDragged = true
setTimeout(_ => {
state.recentlyDragged = false
}, 300)
}
Это делается для того, чтобы метка получила этот последующий щелчок, поскольку при этом можно снять или проверить действие, выполненное пользователем.
Если бы мне пришлось сделать это снова, я мог бы рассмотреть возможность настройки DOM с помощью JavaScript во время обновления UX, чтобы создать элемент, который сам обрабатывает щелчки по меткам и не борется со встроенным поведением.
Этот вид JavaScript мне меньше всего нравится писать, я не хочу управлять условным всплыванием событий:
const preventBubbles = event => {
if (state.recentlyDragged)
event.preventDefault() && event.stopPropagation()
}
Заключение
Этот крохотный компонент-переключатель на данный момент оказался самой трудоемкой из всех задач с графическим интерфейсом! Теперь, когда вы знаете, как я это сделал, как бы вы‽ 🙂
Давайте разнообразим наши подходы и изучим все способы разработки в Интернете. Создайте демо, пришлите мне ссылку в Твиттере , и я добавлю ее в раздел ремиксов сообщества ниже!
Ремиксы сообщества
- @KonstantinRouda с пользовательским элементом: демо и кодом .
- @jhvanderschee с кнопкой: Codepen .
Ресурсы
Найдите исходный код .gui-switch
на GitHub .
Базовый обзор того, как создать гибкий и доступный компонент коммутатора.
В этом посте я хочу поделиться мыслями о том, как создавать компоненты коммутатора. Попробуйте демо .
Если вы предпочитаете видео, вот версия этого поста на YouTube:
Обзор
Переключатель функционирует аналогично флажку, но явно представляет логические состояния включения и выключения.
В этой демонстрации используется <input type="checkbox" role="switch">
для большей части своих функций, преимуществом которой является отсутствие необходимости использования CSS или JavaScript для полной функциональности и доступности. Загрузка CSS обеспечивает поддержку языков с письмом справа налево, вертикальности, анимации и многого другого. Загрузка JavaScript делает переключатель перетаскиваемым и ощутимым.
Пользовательские свойства
Следующие переменные представляют различные части переключателя и их параметры. Как класс верхнего уровня, .gui-switch
содержит пользовательские свойства, используемые во всех дочерних компонентах, и точки входа для централизованной настройки.
Отслеживать
Длина ( --track-size
), отступы и два цвета:
.gui-switch {
--track-size: calc(var(--thumb-size) * 2);
--track-padding: 2px;
--track-inactive: hsl(80 0% 80%);
--track-active: hsl(80 60% 45%);
--track-color-inactive: var(--track-inactive);
--track-color-active: var(--track-active);
@media (prefers-color-scheme: dark) {
--track-inactive: hsl(80 0% 35%);
--track-active: hsl(80 60% 60%);
}
}
Большой палец
Размер, цвет фона и цвета выделения взаимодействия:
.gui-switch {
--thumb-size: 2rem;
--thumb: hsl(0 0% 100%);
--thumb-highlight: hsl(0 0% 0% / 25%);
--thumb-color: var(--thumb);
--thumb-color-highlight: var(--thumb-highlight);
@media (prefers-color-scheme: dark) {
--thumb: hsl(0 0% 5%);
--thumb-highlight: hsl(0 0% 100% / 25%);
}
}
Уменьшение движения
Чтобы добавить четкий псевдоним и уменьшить повторение, пользовательский медиа-запрос с уменьшенными предпочтениями движения можно поместить в пользовательское свойство с помощью плагина PostCSS на основе этого проекта спецификации в Media Queries 5 :
@custom-media --motionOK (prefers-reduced-motion: no-preference);
Разметка
Я решил обернуть свой элемент <input type="checkbox" role="switch">
тегом <label>
, объединив их отношения, чтобы избежать двусмысленности ассоциации флажков и меток, в то же время предоставляя пользователю возможность взаимодействовать с меткой для переключения ввода.
<label for="switch" class="gui-switch">
Label text
<input type="checkbox" role="switch" id="switch">
</label>
<input type="checkbox">
поставляется с предварительно встроенным API и состоянием . Браузер управляет checked
свойством и событиями ввода, такими как oninput
и onchanged
.
Макеты
Flexbox , сетка и пользовательские свойства имеют решающее значение для поддержания стилей этого компонента. Они централизуют значения, дают имена неоднозначным вычислениям или областям и включают небольшой API настраиваемых свойств для простой настройки компонентов.
.gui-switch
Макет верхнего уровня для переключателя — flexbox. Класс .gui-switch
содержит частные и общедоступные пользовательские свойства, которые дети используют для расчета своих макетов.
.gui-switch {
display: flex;
align-items: center;
gap: 2ch;
justify-content: space-between;
}
Расширение и изменение макета флексбокса похоже на изменение любого макета флексбокса. Например, чтобы разместить метки над или под переключателем или изменить flex-direction
:
<label for="light-switch" class="gui-switch" style="flex-direction: column">
Default
<input type="checkbox" role="switch" id="light-switch">
</label>
Отслеживать
Ввод флажка стилизован под дорожку переключения путем удаления его обычного appearance: checkbox
и указания вместо него собственного размера:
.gui-switch > input {
appearance: none;
inline-size: var(--track-size);
block-size: var(--thumb-size);
padding: var(--track-padding);
flex-shrink: 0;
display: grid;
align-items: center;
grid: [track] 1fr / [track] 1fr;
}
Дорожка также создает область дорожки сетки по одной ячейке, на которую может претендовать большой палец.
Большой палец
Стиль « appearance: none
также удаляет визуальную галочку, предоставленную браузером. Этот компонент использует псевдоэлемент и псевдокласс :checked
на входе, чтобы заменить этот визуальный индикатор.
Большой палец — это дочерний псевдоэлемент, прикрепленный к input[type="checkbox"]
и располагающийся поверх дорожки, а не под ней, заявляя область сетки track
:
.gui-switch > input::before {
content: "";
grid-area: track;
inline-size: var(--thumb-size);
block-size: var(--thumb-size);
}
Стили
Пользовательские свойства позволяют использовать универсальный компонент переключателя, который адаптируется к цветовым схемам, языкам с письмом справа налево и предпочтениям движения.
Стили сенсорного взаимодействия
На мобильных устройствах браузеры добавляют функцию выделения касаний и выделения текста к меткам и полям ввода. Это негативно повлияло на стиль и обратную связь по визуальному взаимодействию, необходимые этому переключателю. С помощью нескольких строк CSS я могу удалить эти эффекты и добавить свой собственный стиль cursor: pointer
:
.gui-switch {
cursor: pointer;
user-select: none;
-webkit-tap-highlight-color: transparent;
}
Не всегда целесообразно удалять эти стили, поскольку они могут быть ценной обратной связью при визуальном взаимодействии. Обязательно предоставьте собственные альтернативы, если вы их удалите.
Отслеживать
Стили этого элемента в основном связаны с его формой и цветом, к которым он обращается из родительского .gui-switch
через cascade .
.gui-switch > input {
appearance: none;
border: none;
outline-offset: 5px;
box-sizing: content-box;
padding: var(--track-padding);
background: var(--track-color-inactive);
inline-size: var(--track-size);
block-size: var(--thumb-size);
border-radius: var(--track-size);
}
Широкий выбор вариантов настройки трека переключения можно получить с помощью четырех пользовательских свойств. border: none
добавляется с момента appearance: none
не удаляет границы флажка во всех браузерах.
Большой палец
Элемент большого пальца уже находится на правильном track
, но ему нужны стили круга:
.gui-switch > input::before {
background: var(--thumb-color);
border-radius: 50%;
}
Взаимодействие
Используйте пользовательские свойства, чтобы подготовиться к взаимодействиям, которые будут отображать выделение при наведении и изменение положения большого пальца. Предпочтения пользователя также проверяются перед переходом к стилям выделения при движении или наведении.
.gui-switch > input::before {
box-shadow: 0 0 0 var(--highlight-size) var(--thumb-color-highlight);
@media (--motionOK) { & {
transition:
transform var(--thumb-transition-duration) ease,
box-shadow .25s ease;
}}
}
Положение большого пальца
Пользовательские свойства предоставляют единый механизм размещения ползунка на дорожке. В нашем распоряжении размеры дорожки и ползунка, которые мы будем использовать в расчетах, чтобы обеспечить правильное смещение ползунка внутри дорожки: 0%
и 100%
.
Элемент input
владеет переменной позиции --thumb-position
, а псевдоэлемент большого пальца использует ее как позицию translateX
:
.gui-switch > input {
--thumb-position: 0%;
}
.gui-switch > input::before {
transform: translateX(var(--thumb-position));
}
Теперь мы можем изменить --thumb-position
из CSS и псевдоклассов, предоставляемых в элементах флажков. Поскольку ранее мы условно установили transition: transform var(--thumb-transition-duration) ease
для этого элемента, эти изменения могут анимироваться при изменении:
/* positioned at the end of the track: track length - 100% (thumb width) */
.gui-switch > input:checked {
--thumb-position: calc(var(--track-size) - 100%);
}
/* positioned in the center of the track: half the track - half the thumb */
.gui-switch > input:indeterminate {
--thumb-position: calc(
(var(--track-size) / 2) - (var(--thumb-size) / 2)
);
}
Я думаю, что эта разделенная оркестровка сработала хорошо. Элемент «thumb» связан только с одним стилем — позицией translateX
. Вход может управлять всеми сложностями и вычислениями.
Вертикальный
Поддержка осуществлялась с помощью класса модификатора -vertical
, который добавляет вращение с помощью CSS-преобразований к input
элементу.
Однако 3D-повернутый элемент не меняет общую высоту компонента, что может нарушить компоновку блока. Учитывайте это, используя переменные --track-size
и --track-padding
. Рассчитайте минимальное количество места, необходимое для размещения вертикальной кнопки в макете, как ожидалось:
.gui-switch.-vertical {
min-block-size: calc(var(--track-size) + calc(var(--track-padding) * 2));
& > input {
transform: rotate(-90deg);
}
}
(RTL) справа налево
Мой друг по CSS, Элад Шектер , и я вместе разработали прототип выдвижного бокового меню, используя преобразования CSS, которые обрабатывали языки с письмом справа налево путем переворачивания одной переменной. Мы сделали это потому, что в CSS нет логических преобразований свойств и, возможно, никогда не будет. У Элада возникла отличная идея использовать значение пользовательского свойства для инвертирования процентов, чтобы обеспечить возможность управления одним расположением нашей собственной пользовательской логики для логических преобразований. Я использовал ту же самую технику в этом переключателе и думаю, что она сработала отлично:
.gui-switch {
--isLTR: 1;
&:dir(rtl) {
--isLTR: -1;
}
}
Пользовательское свойство --isLTR
изначально имеет значение 1
, что означает, что оно true
поскольку по умолчанию наш макет расположен слева направо. Затем, используя псевдокласс CSS :dir()
, значение устанавливается равным -1
, когда компонент находится в макете справа налево.
Примените --isLTR
к действию, используя его в функции calc()
внутри преобразования:
.gui-switch.-vertical > input {
transform: rotate(-90deg);
transform: rotate(calc(90deg * var(--isLTR) * -1));
}
Теперь вращение вертикального переключателя учитывает положение противоположной стороны, необходимое для расположения справа налево.
Преобразования translateX
в псевдоэлементе большого пальца также необходимо обновить, чтобы учесть требования противоположной стороны:
.gui-switch > input:checked {
--thumb-position: calc(var(--track-size) - 100%);
--thumb-position: calc((var(--track-size) - 100%) * var(--isLTR));
}
.gui-switch > input:indeterminate {
--thumb-position: calc(
(var(--track-size) / 2) - (var(--thumb-size) / 2)
);
--thumb-position: calc(
((var(--track-size) / 2) - (var(--thumb-size) / 2))
* var(--isLTR)
);
}
Хотя этот подход не сможет удовлетворить все потребности, связанные с такой концепцией, как логические преобразования CSS, он предлагает некоторые принципы DRY для многих случаев использования.
Штаты
Использование встроенного input[type="checkbox"]
не было бы полным без обработки различных состояний, в которых он может находиться: :checked
, :disabled
, :indeterminate
и :hover
. :focus
намеренно оставлен в покое, скорректировано только его смещение; кольцо фокусировки отлично смотрелось в Firefox и Safari:
Проверено
<label for="switch-checked" class="gui-switch">
Default
<input type="checkbox" role="switch" id="switch-checked" checked="true">
</label>
Это состояние представляет собой on
состояние. В этом состоянии фон входного «дорожки» установлен на активный цвет, а положение ползунка установлено на «конец».
.gui-switch > input:checked {
background: var(--track-color-active);
--thumb-position: calc((var(--track-size) - 100%) * var(--isLTR));
}
Неполноценный
<label for="switch-disabled" class="gui-switch">
Default
<input type="checkbox" role="switch" id="switch-disabled" disabled="true">
</label>
Кнопка :disabled
не только визуально выглядит по-другому, но также должна сделать элемент неизменяемым. Неизменяемость взаимодействия недоступна в браузере, но визуальные состояния нуждаются в стилях из-за использования appearance: none
.
.gui-switch > input:disabled {
cursor: not-allowed;
--thumb-color: transparent;
&::before {
cursor: not-allowed;
box-shadow: inset 0 0 0 2px hsl(0 0% 100% / 50%);
@media (prefers-color-scheme: dark) { & {
box-shadow: inset 0 0 0 2px hsl(0 0% 0% / 50%);
}}
}
}
Это состояние сложное, поскольку для него нужны темные и светлые темы как с отключенным, так и с отмеченным состояниями. Я стилистически выбрал минимальные стили для этих состояний, чтобы облегчить бремя поддержки комбинаций стилей.
Неопределенный
Часто забываемое состояние — :indeterminate
, когда флажок не установлен и не снят. Это веселое состояние, оно привлекательное и скромное. Хорошее напоминание о том, что логические состояния могут иметь скрытое промежуточное положение между состояниями.
Установить неопределенный флажок сложно, его может установить только JavaScript:
<label for="switch-indeterminate" class="gui-switch">
Indeterminate
<input type="checkbox" role="switch" id="switch-indeterminate">
<script>document.getElementById('switch-indeterminate').indeterminate = true</script>
</label>
Поскольку для меня это состояние непритязательное и привлекательное, мне показалось уместным расположить большой палец переключателя посередине:
.gui-switch > input:indeterminate {
--thumb-position: calc(
calc(calc(var(--track-size) / 2) - calc(var(--thumb-size) / 2))
* var(--isLTR)
);
}
Наведите указатель мыши
Взаимодействия при наведении должны обеспечивать визуальную поддержку подключенного пользовательского интерфейса, а также указывать направление к интерактивному пользовательскому интерфейсу. Этот переключатель выделяет большой палец полупрозрачным кольцом при наведении курсора на метку или ввод. Затем эта анимация при наведении указывает направление к интерактивному элементу большого пальца.
Эффект «подсветки» достигается с помощью box-shadow
. При наведении неотключенного ввода увеличьте размер --highlight-size
. Если пользователя устраивает движение, мы перемещаем box-shadow
и видим, как она увеличивается, если его не устраивает движение, подсветка появляется мгновенно:
.gui-switch > input::before {
box-shadow: 0 0 0 var(--highlight-size) var(--thumb-color-highlight);
@media (--motionOK) { & {
transition:
transform var(--thumb-transition-duration) ease,
box-shadow .25s ease;
}}
}
.gui-switch > input:not(:disabled):hover::before {
--highlight-size: .5rem;
}
JavaScript
На мой взгляд, интерфейс коммутатора может показаться странным в своей попытке имитировать физический интерфейс, особенно такой, с кругом внутри дорожки. iOS сделала это правильно благодаря своему переключателю: вы можете перетаскивать их из стороны в сторону, и очень приятно иметь такую возможность. И наоборот, элемент пользовательского интерфейса может считаться неактивным, если при попытке перетаскивания ничего не происходит.
Перетаскиваемые большие пальцы
Псевдоэлемент большого пальца получает свою позицию из области .gui-switch > input
var(--thumb-position)
. JavaScript может предоставить значение встроенного стиля на входе, чтобы динамически обновлять положение большого пальца, создавая впечатление, что он следует за жестом указателя. Когда указатель отпущен, удалите встроенные стили и определите, было ли перетаскивание ближе к выключению или включению, используя пользовательское свойство --thumb-position
. Это основа решения; события указателя, условно отслеживающие позиции указателя для изменения пользовательских свойств CSS.
Поскольку до появления этого сценария компонент уже был на 100% функциональным, для поддержания существующего поведения требуется немало усилий, например, нажатие на метку для переключения ввода. Наш JavaScript не должен добавлять функции за счет существующих функций.
touch-action
Перетаскивание — это жест, настраиваемый, что делает его отличным кандидатом на преимущества touch-action
. В случае с этим переключателем наш скрипт должен обрабатывать горизонтальный жест или вертикальный жест, зафиксированный для варианта вертикального переключателя. С помощью touch-action
мы можем указать браузеру, какие жесты обрабатывать на этом элементе, чтобы скрипт мог обрабатывать жест без конкуренции.
Следующий CSS указывает браузеру, что, когда жест указателя начинается с этой дорожки переключения, обрабатывайте вертикальные жесты и ничего не делайте с горизонтальными:
.gui-switch > input {
touch-action: pan-y;
}
Желаемый результат — горизонтальный жест, который не будет также панорамировать или прокручивать страницу. Указатель может прокручивать по вертикали, начиная с входных данных, и прокручивать страницу, но горизонтальные указатель обрабатываются пользователем.
Утилиты для создания стилей значений пикселей
При настройке и во время перетаскивания из элементов необходимо будет получить различные вычисленные числовые значения. Следующие функции JavaScript возвращают вычисленные значения пикселей с учетом свойства CSS. Он используется в сценарии установки, например getStyle(checkbox, 'padding-left')
.
const getStyle = (element, prop) => {
return parseInt(window.getComputedStyle(element).getPropertyValue(prop));
}
const getPseudoStyle = (element, prop) => {
return parseInt(window.getComputedStyle(element, ':before').getPropertyValue(prop));
}
export {
getStyle,
getPseudoStyle,
}
Обратите внимание, как window.getComputedStyle()
принимает второй аргумент — целевой псевдоэлемент. Довольно здорово, что JavaScript может считывать так много значений из элементов, даже из псевдоэлементов.
dragging
Это основной момент для логики перетаскивания, и есть несколько моментов, на которые следует обратить внимание в обработчике событий функции:
const dragging = event => {
if (!state.activethumb) return
let {thumbsize, bounds, padding} = switches.get(state.activethumb.parentElement)
let directionality = getStyle(state.activethumb, '--isLTR')
let track = (directionality === -1)
? (state.activethumb.clientWidth * -1) + thumbsize + padding
: 0
let pos = Math.round(event.offsetX - thumbsize / 2)
if (pos < bounds.lower) pos = 0
if (pos > bounds.upper) pos = bounds.upper
state.activethumb.style.setProperty('--thumb-position', `${track + pos}px`)
}
Герой скрипта — state.activethumb
, маленький кружок, который этот скрипт позиционирует вместе с указателем. Объект switches
— это Map()
, где ключи — это .gui-switch
, а значения — это кэшированные границы и размеры, которые обеспечивают эффективность сценария. Направление справа налево обрабатывается с использованием того же пользовательского свойства, что и CSS — --isLTR
, и может использовать его для инвертирования логики и продолжения поддержки RTL. event.offsetX
также полезен, поскольку содержит значение дельты, полезное для позиционирования ползунка.
state.activethumb.style.setProperty('--thumb-position', `${track + pos}px`)
Эта последняя строка CSS устанавливает пользовательское свойство, используемое элементом большого пальца. В противном случае это присвоение значения будет меняться со временем, но предыдущее событие указателя временно установило --thumb-transition-duration
в 0s
, устраняя то, что было бы вялым взаимодействием.
dragEnd
Чтобы пользователю было разрешено перетащить далеко за пределы переключателя и отпустить его, необходимо зарегистрировать глобальное событие окна:
window.addEventListener('pointerup', event => {
if (!state.activethumb) return
dragEnd(event)
})
Я думаю, что очень важно, чтобы у пользователя была свобода перетаскивания, и чтобы интерфейс был достаточно умным, чтобы это учитывать. С этим переключателем справиться с этой проблемой не составило особого труда, но в процессе разработки это потребовало тщательного рассмотрения.
const dragEnd = event => {
if (!state.activethumb) return
state.activethumb.checked = determineChecked()
if (state.activethumb.indeterminate)
state.activethumb.indeterminate = false
state.activethumb.style.removeProperty('--thumb-transition-duration')
state.activethumb.style.removeProperty('--thumb-position')
state.activethumb.removeEventListener('pointermove', dragging)
state.activethumb = null
padRelease()
}
Взаимодействие с элементом завершено, пришло время установить свойство ввода проверено и удалить все события жестов. Флажок изменяется с помощью state.activethumb.checked = determineChecked()
.
determineChecked()
Эта функция, вызываемая функцией dragEnd
, определяет, где ток ползунка находится в пределах его дорожки, и возвращает true, если он равен или превышает половину длины дорожки:
const determineChecked = () => {
let {bounds} = switches.get(state.activethumb.parentElement)
let curpos =
Math.abs(
parseInt(
state.activethumb.style.getPropertyValue('--thumb-position')))
if (!curpos) {
curpos = state.activethumb.checked
? bounds.lower
: bounds.upper
}
return curpos >= bounds.middle
}
Дополнительные мысли
Жест перетаскивания вызвал некоторую задолженность по коду из-за выбранной исходной структуры HTML, в основном заключающей в себя вводимые данные в метку. Метка, являющаяся родительским элементом, будет получать взаимодействия с щелчками после ввода. В конце события dragEnd
вы, возможно, заметили, что функция padRelease()
звучит странно.
const padRelease = () => {
state.recentlyDragged = true
setTimeout(_ => {
state.recentlyDragged = false
}, 300)
}
Это делается для того, чтобы метка получила этот последующий щелчок, поскольку при этом можно снять или проверить действие, выполненное пользователем.
Если бы мне пришлось сделать это снова, я мог бы рассмотреть возможность настройки DOM с помощью JavaScript во время обновления UX, чтобы создать элемент, который сам обрабатывает щелчки по меткам и не борется со встроенным поведением.
Этот вид JavaScript мне меньше всего нравится писать, я не хочу управлять условным всплыванием событий:
const preventBubbles = event => {
if (state.recentlyDragged)
event.preventDefault() && event.stopPropagation()
}
Заключение
Этот крохотный компонент-переключатель на данный момент оказался самой трудоемкой из всех задач с графическим интерфейсом! Теперь, когда вы знаете, как я это сделал, как бы вы‽ 🙂
Давайте разнообразим наши подходы и изучим все способы разработки в Интернете. Создайте демо, пришлите мне ссылку в Твиттере , и я добавлю ее в раздел ремиксов сообщества ниже!
Ремиксы сообщества
- @KonstantinRouda с пользовательским элементом: демо и кодом .
- @jhvanderschee с кнопкой: Codepen .
Ресурсы
Найдите исходный код .gui-switch
на GitHub .
Базовый обзор того, как создать гибкий и доступный компонент коммутатора.
В этом посте я хочу поделиться мыслями о том, как создавать компоненты коммутатора. Попробуйте демо .
Если вы предпочитаете видео, вот версия этого поста на YouTube:
Обзор
Переключатель функционирует аналогично флажку, но явно представляет логические состояния включения и выключения.
В этой демонстрации используется <input type="checkbox" role="switch">
для большей части своих функций, преимуществом которой является отсутствие необходимости использования CSS или JavaScript для полной функциональности и доступности. Загрузка CSS обеспечивает поддержку языков с письмом справа налево, вертикальности, анимации и многого другого. Загрузка JavaScript делает переключатель перетаскиваемым и ощутимым.
Пользовательские свойства
Следующие переменные представляют различные части переключателя и их параметры. Как класс верхнего уровня, .gui-switch
содержит пользовательские свойства, используемые во всех дочерних компонентах, и точки входа для централизованной настройки.
Отслеживать
Длина ( --track-size
), отступы и два цвета:
.gui-switch {
--track-size: calc(var(--thumb-size) * 2);
--track-padding: 2px;
--track-inactive: hsl(80 0% 80%);
--track-active: hsl(80 60% 45%);
--track-color-inactive: var(--track-inactive);
--track-color-active: var(--track-active);
@media (prefers-color-scheme: dark) {
--track-inactive: hsl(80 0% 35%);
--track-active: hsl(80 60% 60%);
}
}
Большой палец
Размер, цвет фона и цвета выделения взаимодействия:
.gui-switch {
--thumb-size: 2rem;
--thumb: hsl(0 0% 100%);
--thumb-highlight: hsl(0 0% 0% / 25%);
--thumb-color: var(--thumb);
--thumb-color-highlight: var(--thumb-highlight);
@media (prefers-color-scheme: dark) {
--thumb: hsl(0 0% 5%);
--thumb-highlight: hsl(0 0% 100% / 25%);
}
}
Уменьшение движения
Чтобы добавить четкий псевдоним и уменьшить повторение, пользовательский медиа-запрос с уменьшенными предпочтениями движения можно поместить в пользовательское свойство с помощью плагина PostCSS на основе этого проекта спецификации в Media Queries 5 :
@custom-media --motionOK (prefers-reduced-motion: no-preference);
Разметка
Я решил обернуть свой элемент <input type="checkbox" role="switch">
тегом <label>
, объединив их отношения, чтобы избежать двусмысленности ассоциации флажков и меток, в то же время предоставляя пользователю возможность взаимодействовать с меткой для переключения ввода.
<label for="switch" class="gui-switch">
Label text
<input type="checkbox" role="switch" id="switch">
</label>
<input type="checkbox">
поставляется с предварительно встроенным API и состоянием . Браузер управляет checked
свойством и входными событиями, такими как oninput
и onchanged
.
Макеты
Flexbox , Grid и пользовательские свойства имеют решающее значение для поддержания стилей этого компонента. Они централизуют значения, дают имена неоднозначным расчетам или областям и позволяют небольшую пользовательскую API свойства для удобной настройки компонентов.
.gui-switch
Макет верхнего уровня для переключателя-Flexbox. Class .gui-switch
содержит частные и общедоступные пользовательские свойства, которые дети используют для вычисления своих макетов.
.gui-switch {
display: flex;
align-items: center;
gap: 2ch;
justify-content: space-between;
}
Расширение и изменение макета Flexbox - это все равно, что изменить любую компоновку Flexbox. Например, чтобы поместить этикетки выше или ниже переключателя или изменить flex-direction
:
<label for="light-switch" class="gui-switch" style="flex-direction: column">
Default
<input type="checkbox" role="switch" id="light-switch">
</label>
Отслеживать
Вход в флажок стилизован как дорожка коммутатора, удаляя его обычный appearance: checkbox
и вместо этого поставляя свой собственный размер:
.gui-switch > input {
appearance: none;
inline-size: var(--track-size);
block-size: var(--thumb-size);
padding: var(--track-padding);
flex-shrink: 0;
display: grid;
align-items: center;
grid: [track] 1fr / [track] 1fr;
}
Трек также создает область сетки с сети с одной ячейкой для большого пальца.
Большой палец
appearance: none
также удаляет визуальную галочку, поставляемую браузером. В этом компоненте используется псевдоэлемент и :checked
псевдо-класс на входе, чтобы заменить этот визуальный индикатор.
Большой палец-это псевдоэлементный ребенок, прикрепленный к input[type="checkbox"]
и стеки поверх дорожки, а не под ним, заявив о track
области сетки:
.gui-switch > input::before {
content: "";
grid-area: track;
inline-size: var(--thumb-size);
block-size: var(--thumb-size);
}
Стили
Пользовательские свойства включают универсальный компонент коммутатора, который адаптируется к цветовым схемам, языкам справа налево и предпочтениям движения.
Прикоснитесь к стилям взаимодействия
На мобильных устройствах браузеры добавляют основные моменты и функции выбора текста в этикетки и входы. Они отрицательно повлияли на обратную связь по стилю и визуальному взаимодействию, которые нуждались в этом переключателе. С несколькими линиями CSS я могу удалить эти эффекты и добавить свой собственный cursor: pointer
:
.gui-switch {
cursor: pointer;
user-select: none;
-webkit-tap-highlight-color: transparent;
}
Не всегда целесообразно удалить эти стили, так как они могут быть ценной обратной связью с визуальным взаимодействием. Обязательно предоставьте пользовательские альтернативы, если вы их удалите.
Отслеживать
Стили этого элемента в основном о его форме и цвете, которые он обращается с родительским .gui-switch
через каскад .
.gui-switch > input {
appearance: none;
border: none;
outline-offset: 5px;
box-sizing: content-box;
padding: var(--track-padding);
background: var(--track-color-inactive);
inline-size: var(--track-size);
block-size: var(--thumb-size);
border-radius: var(--track-size);
}
Широкий спектр параметров настройки для дорожки Switch поступает из четырех пользовательских свойств. border: none
не добавляется, так как appearance: none
не удаляет границы с флажона во всех браузерах.
Большой палец
Элемент большого пальца уже находится на правильном track
, но нуждается в стилях круга:
.gui-switch > input::before {
background: var(--thumb-color);
border-radius: 50%;
}
Взаимодействие
Используйте пользовательские свойства, чтобы подготовиться к взаимодействию, которые покажут подсветки на покачивании и изменения позиции большого пальца. Предпочтение пользователя также проверяется перед переходом стилей Motion или Hover.
.gui-switch > input::before {
box-shadow: 0 0 0 var(--highlight-size) var(--thumb-color-highlight);
@media (--motionOK) { & {
transition:
transform var(--thumb-transition-duration) ease,
box-shadow .25s ease;
}}
}
Положение большого пальца
Пользовательские свойства обеспечивают один исходный механизм для позиционирования большого пальца на трассе. В нашем распоряжении размеры трассы и больших пальцев, которые мы будем использовать в расчетах, чтобы большой палец был должным образом смещен и между трассой: 0%
и 100%
.
input
элемент владеет переменной позиции --thumb-position
, а псевдо-элемент большого пальца использует ее в качестве позиции translateX
:
.gui-switch > input {
--thumb-position: 0%;
}
.gui-switch > input::before {
transform: translateX(var(--thumb-position));
}
Теперь мы свободны изменяться --thumb-position
от CSS и псевдо-классов, предоставленных в элементах флажона. Поскольку мы условно устанавливаем transition: transform var(--thumb-transition-duration) ease
на этом элементе, эти изменения могут оживить при изменении:
/* positioned at the end of the track: track length - 100% (thumb width) */
.gui-switch > input:checked {
--thumb-position: calc(var(--track-size) - 100%);
}
/* positioned in the center of the track: half the track - half the thumb */
.gui-switch > input:indeterminate {
--thumb-position: calc(
(var(--track-size) / 2) - (var(--thumb-size) / 2)
);
}
Я думал, что эта отдельная оркестровка сработала хорошо. Элемент большого пальца связан только с одним стилем, позицией translateX
. Ввод может управлять всей сложностью и расчетами.
Вертикальный
Поддержка была сделана с помощью класса -vertical
, которая добавляет вращение с преобразованием CSS к input
элементу.
3D -вращающийся элемент не изменяет общую высоту компонента, который может отбросить макет блока. Учитете это с использованием переменных --track-size
и --track-padding
. Рассчитайте минимальное количество пространства, необходимого для вертикальной кнопки, течь в макете, как и ожидалось:
.gui-switch.-vertical {
min-block-size: calc(var(--track-size) + calc(var(--track-padding) * 2));
& > input {
transform: rotate(-90deg);
}
}
(RTL) Право на летя
Друг CSS, Elad Schecter , и я прототипировал прототип вместе выдвижного бокового меню, используя CSS-преобразования, которые обрабатывают языки с правой к лета, путем переворачивания одной переменной. Мы сделали это, потому что в CSS нет логических преобразований свойств, и, возможно, никогда не будет. У Elad была отличная идея использования пользовательской стоимости свойства для инвертирования процентов, чтобы разрешить управление отдельным местоположением нашей собственной собственной логики для логических преобразований. Я использовал эту же технику в этом переключателе, и я думаю, что это сработало отлично:
.gui-switch {
--isLTR: 1;
&:dir(rtl) {
--isLTR: -1;
}
}
Пользовательское свойство под названием --isLTR
Первоначально имеет значение 1
, что означает, что оно true
поскольку наш макет по умолчанию остается правой. Затем, используя класс CSS Pseudo :dir()
, значение установлено на -1
, когда компонент находится в пределах сразу.
--isLTR
в действие в действие, используя его в calc()
внутри преобразования:
.gui-switch.-vertical > input {
transform: rotate(-90deg);
transform: rotate(calc(90deg * var(--isLTR) * -1));
}
Теперь вращение вертикального переключателя учитывает противоположную сторону положения, требуемой для макета справа налево.
Преобразование translateX
на псевдоэлементу большого пальца также необходимо обновить, чтобы учесть требование противоположной стороны:
.gui-switch > input:checked {
--thumb-position: calc(var(--track-size) - 100%);
--thumb-position: calc((var(--track-size) - 100%) * var(--isLTR));
}
.gui-switch > input:indeterminate {
--thumb-position: calc(
(var(--track-size) / 2) - (var(--thumb-size) / 2)
);
--thumb-position: calc(
((var(--track-size) / 2) - (var(--thumb-size) / 2))
* var(--isLTR)
);
}
Несмотря на то, что этот подход не будет работать, чтобы решить все потребности в отношении концепции, такой как логические преобразования CSS, он предлагает некоторые сухие принципы для многих вариантов использования.
Штаты
Использование встроенного input[type="checkbox"]
не будет полным без обработки различных состояний, он может быть в :: :checked
,: :disabled
,: :indeterminate
и :hover
. :focus
был намеренно оставлен в покое, а корректировка была сделана только в его смещении; Кольцо фокуса отлично смотрелось на Firefox и Safari:
Проверено
<label for="switch-checked" class="gui-switch">
Default
<input type="checkbox" role="switch" id="switch-checked" checked="true">
</label>
Это состояние представляет собой on
. В этом состоянии фон «трек» ввода устанавливается на активный цвет, а положение большого пальца установлено на «Конец».
.gui-switch > input:checked {
background: var(--track-color-active);
--thumb-position: calc((var(--track-size) - 100%) * var(--isLTR));
}
Неполноценный
<label for="switch-disabled" class="gui-switch">
Default
<input type="checkbox" role="switch" id="switch-disabled" disabled="true">
</label>
A :disabled
не только визуально выглядит по -разному, но и должна сделать элемент непостоянной. Необходимость. Невыразимость не составляет браузера, но визуальные состояния нуждаются в стилях из -за использования appearance: none
.
.gui-switch > input:disabled {
cursor: not-allowed;
--thumb-color: transparent;
&::before {
cursor: not-allowed;
box-shadow: inset 0 0 0 2px hsl(0 0% 100% / 50%);
@media (prefers-color-scheme: dark) { & {
box-shadow: inset 0 0 0 2px hsl(0 0% 0% / 50%);
}}
}
}
Это состояние сложно, поскольку ему нужны темные и легкие темы как с отключенными, так и с проверенными состояниями. Я стилистически выбрал минимальные стили для этих состояний, чтобы облегчить бремя обслуживания комбинаций стилей.
Неопределенный
Часто забытое состояние :indeterminate
, где флажок не проверяется и не проверяется. Это забавное состояние, оно привлекательно и скромно. Хорошее напоминание о том, что логические штаты могут быть подыжительными между штатами.
Сложно установить флажок на неопределенный, только JavaScript может установить его:
<label for="switch-indeterminate" class="gui-switch">
Indeterminate
<input type="checkbox" role="switch" id="switch-indeterminate">
<script>document.getElementById('switch-indeterminate').indeterminate = true</script>
</label>
Поскольку государство, для меня, не является непритязательным и привлекательным, было бы уместно поместить положение большого пальца переключателя в середину:
.gui-switch > input:indeterminate {
--thumb-position: calc(
calc(calc(var(--track-size) / 2) - calc(var(--thumb-size) / 2))
* var(--isLTR)
);
}
Наведите указатель мыши
Взаимодействие Hover должно обеспечивать визуальную поддержку подключенного пользовательского интерфейса, а также обеспечивать направление к интерактивному пользовательскому интерфейсу. Этот переключатель выделяет большой палец с полупрозрачным кольцом, когда на метелю или вход падают. Затем эта анимация на падении обеспечивает направление к интерактивному элементу большого пальца.
Эффект «выделения» выполняется с помощью box-shadow
. На Hover, не инвалидом вход, увеличьте размер --highlight-size
. Если пользователь в порядке с движением, мы переходим box-shadow
и видим, как он растут, если он не в порядке с движением, выделяется мгновенно:
.gui-switch > input::before {
box-shadow: 0 0 0 var(--highlight-size) var(--thumb-color-highlight);
@media (--motionOK) { & {
transition:
transform var(--thumb-transition-duration) ease,
box-shadow .25s ease;
}}
}
.gui-switch > input:not(:disabled):hover::before {
--highlight-size: .5rem;
}
JavaScript
Для меня интерфейс коммутатора может чувствовать себя странно в попытке подражать физическому интерфейсу, особенно такого рода с кругом внутри дорожки. IOS получил это правильно с их переключателем, вы можете перетащить их из стороны в сторону, и очень приятно иметь эту опцию. И наоборот, элемент пользовательского интерфейса может чувствовать себя неактивным, если попытка жеста перетаскивания и ничего не произойдет.
Перетаскиваемые большие пальцы
Псевдоэлемент большого пальца получает свою позицию от VAR .gui-switch > input
зоны var(--thumb-position)
, JavaScript может предоставить значение входного стиля для динамического обновления позиции большого пальца, заставляя его следовать за жестом указателя. Когда указатель выпускается, удалите встроенные стили и определите, было ли перетаскивание ближе к выключению или включению с помощью пользовательского свойства --thumb-position
. Это основа решения; События указателей условное отслеживание позиций указателя для изменения пользовательских свойств CSS.
Поскольку компонент уже был на 100% функциональным до того, как этот скрипт будет отображаться, требуется немало работы, чтобы поддерживать существующее поведение, например, щелкнуть метку, чтобы переключить вход. Наш JavaScript не должен добавлять функции за счет существующих функций.
touch-action
Перетаскивание-это жест, нестандартный, который делает его отличным кандидатом на touch-action
. В случае этого переключателя горизонтальный жест следует обрабатывать с помощью нашего сценария или вертикального жеста, захваченного для варианта вертикального переключателя. С touch-action
мы можем рассказать браузеру, какие жесты справиться с этим элементом, поэтому сценарий может справиться с жестом без конкуренции.
Следующий CSS инструктирует браузер, что, когда жест указателя начинается из этой дорожки переключателя, обрабатывают вертикальные жесты, ничего не делают с горизонтальными:
.gui-switch > input {
touch-action: pan-y;
}
Желаемый результат - горизонтальный жест, который также не опускается и не прокручивает страницу. Указатель может вертикально прокрутить из входа и прокрутить страницу, но горизонтальные обрабатываются.
Пиксельные утилиты в стиле ценности
При настройке и во время перетаскивания различные вычисленные номера должны быть захвачены из элементов. Следующие функции JavaScript возвращают вычисленные значения пикселей с учетом свойства CSS. Он используется в сценарии настройки, как этот getStyle(checkbox, 'padding-left')
.
const getStyle = (element, prop) => {
return parseInt(window.getComputedStyle(element).getPropertyValue(prop));
}
const getPseudoStyle = (element, prop) => {
return parseInt(window.getComputedStyle(element, ':before').getPropertyValue(prop));
}
export {
getStyle,
getPseudoStyle,
}
Обратите внимание, как window.getComputedStyle()
принимает второй аргумент, целевой псевдо -элемент. Довольно аккуратно, что JavaScript может прочитать так много значений из элементов, даже из псевдо -элементов.
dragging
Это основной момент для логики перетаскивания, и есть несколько вещей, отмечающих обработчик событий функции:
const dragging = event => {
if (!state.activethumb) return
let {thumbsize, bounds, padding} = switches.get(state.activethumb.parentElement)
let directionality = getStyle(state.activethumb, '--isLTR')
let track = (directionality === -1)
? (state.activethumb.clientWidth * -1) + thumbsize + padding
: 0
let pos = Math.round(event.offsetX - thumbsize / 2)
if (pos < bounds.lower) pos = 0
if (pos > bounds.upper) pos = bounds.upper
state.activethumb.style.setProperty('--thumb-position', `${track + pos}px`)
}
Герой сценария - это state.activethumb
. Объект switches
-это Map()
, где клавиши являются .gui-switch
's, а значения являются кэшированными границами и размерами, которые сохраняют эффективный скрипт. Право к лету обрабатывается с использованием того же собственного свойства, что CSS- --isLTR
, и может использовать его для инвертирования логики и продолжать поддержку RTL. event.offsetX
также является ценным, так как он содержит дельта -значение, полезное для позиционирования большого пальца.
state.activethumb.style.setProperty('--thumb-position', `${track + pos}px`)
Эта последняя строка CSS устанавливает пользовательское свойство, используемое элементом большого пальца. Это назначение значения в противном случае переходило бы со временем, но предыдущее событие указателя временно установило --thumb-transition-duration
до 0s
, удаляя то, что было бы вялым взаимодействием.
dragEnd
Для того чтобы пользователю разрешили перетаскивать далеко за пределы переключателя и отпустить, необходимо зарегистрировано глобальное окно -событие:
window.addEventListener('pointerup', event => {
if (!state.activethumb) return
dragEnd(event)
})
Я думаю, что очень важно, чтобы пользователь обладал свободой свободно перетаскивать и чтобы интерфейс был достаточно умным, чтобы учесть его. Это не потребовалось много, чтобы справиться с этим с этим переключателем, но он нуждался в тщательном рассмотрении во время процесса разработки.
const dragEnd = event => {
if (!state.activethumb) return
state.activethumb.checked = determineChecked()
if (state.activethumb.indeterminate)
state.activethumb.indeterminate = false
state.activethumb.style.removeProperty('--thumb-transition-duration')
state.activethumb.style.removeProperty('--thumb-position')
state.activethumb.removeEventListener('pointermove', dragging)
state.activethumb = null
padRelease()
}
Взаимодействие с элементом завершено, время для установки входного свойства и удаления всех событий жеста. Флажок изменяется с помощью state.activethumb.checked = determineChecked()
.
determineChecked()
Эта функция, вызванная dragEnd
, определяет, где ток большого пальца находится в пределах границ его дорожки, и возвращает True, если она равна или через полпути вдоль трассы:
const determineChecked = () => {
let {bounds} = switches.get(state.activethumb.parentElement)
let curpos =
Math.abs(
parseInt(
state.activethumb.style.getPropertyValue('--thumb-position')))
if (!curpos) {
curpos = state.activethumb.checked
? bounds.lower
: bounds.upper
}
return curpos >= bounds.middle
}
Дополнительные мысли
Жест перетаскивания понесла немного задолженности по коде из -за исходной выбранной структуры HTML, в основном, в основном, вводя вклад в этикетку. Метка, будучи родительским элементом, будет получать взаимодействия Click после ввода. В конце события dragEnd
вы, возможно, заметили padRelease()
как странную функцию звучания.
const padRelease = () => {
state.recentlyDragged = true
setTimeout(_ => {
state.recentlyDragged = false
}, 300)
}
Это должно учитывать этикетку, получившую это более позднее щелчок, так как он снял бы сделку или проверяет взаимодействие, которое выполнял пользователь.
Если бы я сделал это снова, я мог бы подумать о настройке DOM с помощью JavaScript во время обновления UX, чтобы создать элемент, который обрабатывает этикетку, нажимает себя и не борется со встроенным поведением.
Этот вид javaScript - мой наименее любимый для написания, я не хочу управлять пузырьками условного события:
const preventBubbles = event => {
if (state.recentlyDragged)
event.preventDefault() && event.stopPropagation()
}
Заключение
Этот маленький компонент Switch в конечном итоге стал наибольшей работой из всех проблем с графическим интерфейсом до сих пор! Теперь, когда вы знаете, как я это сделал, как бы вы‽ 🙂
Давайте разнообразим наши подходы и изучим все способы разработки в Интернете. Создайте демо, пришлите мне ссылку в Твиттере , и я добавлю ее в раздел ремиксов сообщества ниже!
Ремиксы сообщества
- @Konstantinrouda с пользовательским элементом: демонстрация и код .
- @jhvanderschee с кнопкой: Codepen .
Ресурсы
Найдите исходный код .gui-switch
на GitHub .