Как улучшить UX с помощью поля выбора прокрутки
Поле выбора с помощью прокрутки - отличный инструмент для разработчиков интерфейсов, позволяющий улучшить работу пользователей с их приложениями.
Нужен способ, позволяющий пользователям выбирать нужные элементы из списка? Просто используйте обычный выпадающий элемент <выбрать>. Вам знакомо маленькое окошко, которое открывается в виде списка вариантов, не занимает много места и имеет встроенную клавиатуру для навигации? Да, вот этот!
Но теперь давайте представим, что у нас есть длинный список вариантов (возможно, длинный список лет рождения) на выбор. Конечно, есть навигация с клавиатуры. Но давайте будем честны: кто будет просматривать сотни вариантов? Большинство людей (особенно я) просто пролистают страницу.
Итак, у нас остается только один вариант - прокрутить и выбрать. Это неплохо, но что, если мы сделаем это интересным, сделав так, чтобы опция автоматически выбиралась по мере ее прокрутки?
Вместо традиционного "Прокрутить, остановить, щелкнуть" вы просто прокручиваете, пока не увидите то, что хотите, и - бум! - оно выбрано. Простое изменение, но оно делает весь процесс более плавным и, честно говоря, даже забавным.
Вот пример того, что я имею в виду: поле выбора прокрутки, также известное как элемент управления формой прокрутки для выбора:
Это именно то, что мы собираемся создать сегодня: прокручиваемый инструмент выбора даты, который имитирует стиль iOS, но за исключением элемента <select>. Вместо этого мы будем использовать в основном CSS и JavaScript для создания нашей формы прокрутки для выбора только потому, что она более настраиваема.
Основные концепции окна выбора с помощью прокрутки
Прежде чем углубиться в код, давайте разберемся с ключевыми понятиями, которые мы будем использовать для создания нашего окна выбора прокрутки:
Прокрутка привязки CSS
Свойство scroll snap CSS позволяет нам создавать плавные эффекты прокрутки, определяя "Точки прокрутки", в которых окно просмотра останавливается после того, как пользователь заканчивает прокрутку. Для моих коллег-завсегдатаев TikTok это то, что происходит всякий раз, когда мы прокручиваем видео. Хотя видео становится более красивым, когда оно немного медленное, само по себе TikTok работает довольно быстро (что понятно для его варианта использования).
Наблюдатель за перекрестком
API Intersection Observer больше похож на глаз, который следит за тем, какие параметры отображаются в поле зрения. Технически, он позволяет нам определять, когда элементы входят в область просмотра или покидают ее. Мы будем использовать это, чтобы определить, какой параметр следует выбрать при прокрутке страницы пользователем.
Его реализация выглядит следующим образом:
const observer = new IntersectionObserver((entries) => { entries.forEach(entry => { if (entry.isIntersecting) { // Hey, this option is visible, do somehting to it! selectOption(entry.target); } }); });
Пользовательские свойства (переменные CSS)
В этой статье пользовательские свойства CSS будут использоваться для поддержания единообразия стилей во всем приложении. Если это для вас в новинку, то это похоже на простую систему проектирования, которая имеет переменную и использует свойства стиля. Все, что нам нужно сделать, это изменить свойство переменной, и все остальные свойства в нашем приложении автоматически обновятся. Это отлично подходит для создания надежных стилей.
Остальные наши основные элементы - это стили и логика, которые вы можете настроить по своему вкусу. Давайте настроим нашу HTML-структуру, таблицу стилей ссылок и скрипт.
Настройка краткой структуры HTML
Мы будем использовать простую структуру, в которой каждый элемент выбора (месяц, день и год) будет соответствовать одному и тому же шаблону:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Scroll-to-select-form by Logrocket</title> <link rel="stylesheet" href="styles.css"> </head> <body> <h1>Logrocket Scroll To select Date Picker</h1> <div class="date-picker-container"> <!-- Month Selector --> <div class="custom-select" id="monthSelect"> <div class="select-display">Month</div> <div class="options-selector"></div> </div> <!-- Day Selector --> <div class="custom-select" id="daySelect"> <div class="select-display">Day</div> <div class="options-selector"></div> </div> <!-- Year Selector --> <div class="custom-select" id="yearSelect"> <div class="select-display">Year</div> <div class="options-selector"></div> </div> </div> <div class="selected-date" id="selectedDate">Select a date</div> <script src="script.js"></script> </body> </html>
Вместо использования элементов <select>, описанных выше, мы используем пользовательские разделители. Позже это поможет нам создавать прокручиваемые параметры с помощью JavaScript. На дисплее .select отображается текущий выбор, в то время как .options-selector будет содержать наши прокручиваемые параметры. Далее мы рассмотрим стиль.
Стилизация нашей формы прокрутки для выбора
Давайте настроим наши базовые стили и объявим нашу переменную CSS для нашего окна выбора прокрутки:
/* Root variables with color scheme */ :root { --primary-color: #9c27b0; /* Purple */ --secondary-color: #e1bee7; --gradient-start: #ba68c8; --gradient-end: #7b1fa2; --container-width: 210px; --item-height: 40px; --spacing: 10px; } /* Reset default styles */ * { margin: 0; padding: 0; box-sizing: border-box; } /* Base layout styles */ body { min-height: 100vh; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 2rem; font-family: system-ui, -apple-system, sans-serif; -webkit-user-select: none; user-select: none; background-color: #fafafa; } h1 { font-size: 1.5rem; color: var(--primary-color); } /* Date picker container */ .date-picker-container { display: flex; gap: 1rem; align-items: flex-start; }
Приведенный выше код создает основу для нашей формы. В основной части мы расположили содержимое по центру, используя систему верстки CSS Flexbox, на светло-сером фоне. В нашем h1 мы оформили заголовок фиолетовым цветом, определенным в наших переменных выше.
В нашем контейнере.date-picker-container мы создали горизонтальный макет для трех выпадающих списков (месяц, день и год).
Свойство -webkit-user-select: none дает нам ощущение собственного приложения, предотвращая выделение текста во время прокрутки. Если этих основных технических слов недостаточно, то все, что нужно сделать с помощью кода, - это выбрать цвет, размер и убедиться, что все это хорошо расположено по центру страницы.
Переходя к стилям, мы захотим создать эти видимые кнопки для наших селекторов месяца/дня/года:
/* Custom select styles */ .custom-select { position: relative; width: var(--container-width); } /* Selected value display */ .select-display { width: 100%; height: var(--item-height); padding: 0 1rem; background: linear-gradient(to right, var(--gradient-start), var(--gradient-end)); color: white; border-radius: 6px; display: flex; align-items: center; justify-content: space-between; cursor: pointer; font-size: 1.25rem; box-shadow: 0 2px 5px rgba(156, 39, 176, 0.2); }
В приведенном выше коде мы привязали относительное положение к .custom-select. Это важно, потому что помогает расположить выпадающее меню, которое появляется ниже при нажатии.
Когда пользователь видит "Январь", "15-е число" или "2025-е", на дисплее .select выполняется настройка стиля. Кнопки имеют фиолетовый градиент, белый текст и небольшую тень, из-за которой они кажутся "плавающими".
Всякий раз, когда наша форма открыта, т.е. пользователь нажимает дату, год или месяц, мы хотим прикрепить стрелку вниз () к каждой из этих кнопок, поворачивая ее на 180°.
Но почему это здесь, спросите вы? Всякий раз, когда при отображении параметров выбирается месяц/день/год, стрелка поворачивается на 180°, указывая либо на открытое, либо на закрытое состояние:
.select-display::after { content: 'â¼'; font-size: 0.8em; transition: transform 0.3s ease; } .custom-select.open .select-display::after { transform: rotate(180deg); }
В приведенном выше коде переход делает вращение плавным, а не мгновенным. Кто-то может спросить: как CSS сделает это вращение интерактивным? На самом деле CSS не стал бы этого делать; это сделал бы JavaScript. Для лучшего понимания мы хотим закончить со всем, что касается CSS, прежде чем перейдем к Javascript.
Мы продолжим и оформим наш выпадающий список ниже:
/* Options dropdown */ .options-selector { position: absolute; top: calc(var(--item-height) + var(--spacing)); width: 100%; height: calc(var(--item-height) * 7 + var(--spacing) * 6); overflow-y: auto; scroll-snap-type: y mandatory; overscroll-behavior-y: none; border-radius: 8px; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); padding: var(--spacing); background: white; /* Hide scrollbar for different browsers */ &::-webkit-scrollbar { display: none; } -ms-overflow-style: none; scrollbar-width: none; /* Animation states */ opacity: 0; visibility: hidden; transform: translateY(-10px); transition: all 0.3s ease; z-index: 100; } .custom-select.open .options-selector { opacity: 1; visibility: visible; transform: translateY(0); }
Здесь все становится еще интереснее. Мы используем position: absolute, чтобы наш выпадающий список отображался поверх остального содержимого. Расчет высоты предназначен для отображения ровно семи элементов одновременно. Я обнаружил, что это мое любимое место с точки зрения удобства использования.
Как упоминалось ранее, функция scroll-snap-type: y в обязательном порядке используется для создания того приятного эффекта привязки, который вы ощущаете при прокрутке параметров на вашем телефоне.
Режим избыточной прокрутки-y: none - это просто хорошие манеры; он останавливает прокрутку всей страницы, когда вы доходите до конца списка наших опций.
Мы хотим, чтобы выпадающий список плавно исчезал. Вот тут-то и появляется анимация. Непрозрачность, видимость и трансформация отвечают за плавное включение / выключение, когда мы переключаем наш выпадающий список.
Что касается отдельных параметров, мы хотим, чтобы они выглядели интерактивными и соответствовали стилям при выборе:
/* Option items */ .option-item { display: flex; align-items: center; justify-content: center; height: var(--item-height); margin-bottom: var(--spacing); background: linear-gradient(to right, var(--gradient-start), var(--gradient-end)); border-radius: 6px; color: white; font-size: 1.25rem; scroll-snap-align: start; //explained below transition: background-color 0.3s ease; cursor: pointer; } .option-item:last-child { margin-bottom: calc(var(--item-height) * 6); }
В приведенном выше коде мы снабдили каждый параметр красивым градиентным фоном и переходами. В flexbox все идеально выровнено. Обычно это хорошая практика для любого типа кода.
Свойство transition - это то, что обеспечивает плавное изменение цвета при прокрутке или выборе параметра, а также для параметра .option-item:last-child. Это свойство добавляет дополнительный пробел после последнего параметра в выпадающем списке.
Когда опция устанавливается на место в нашей демонстрации, она меняет цвет и масштаб примерно на 7%. Давайте исправим это ниже с помощью нескольких стилей:
.option-item.selected { background: var(--primary-color); transform: scale(1.08); transition: all 0.3s ease; }
Ниже я выделил свойство scroll-snap-align из-за его важности:
scroll-snap-align: start;
Свойство scroll-snap-align указывает браузеру, где должен отображаться каждый параметр при прокрутке. Если установить его в start, это означает, что каждый параметр будет выровнен по верхней части нашего контейнера, создавая именно тот эффект прокрутки. Без этого наша система привязки к типу прокрутки не знала бы, к чему привязываться. Они работают вместе, как одна команда, чтобы создать такой эффект прокрутки.
Для нашего шрифта .selected-date мы хотим просто добавить небольшое поле сверху, придать ему наш основной цвет и в целом сделать так, чтобы он выглядел красиво:
/* Selected date display */ .selected-date { margin-top: 2rem; color: var(--primary-color); font-size: 1.2rem; font-weight: 500; }
Вот как выглядит наше приложение:
Сейчас он не очень интерактивен, потому что мы еще не внедрили JavaScript. Вот и все, что касается стилизации; давайте перейдем к действительно интересной части проекта "окно выбора прокрутки".
Взаимодействие с JavaScript
В этом разделе мы сделаем наше приложение интерактивным с помощью JavaScript. Для начала нам понадобится список месяцев и данные за год. Допустим, в этом примере мы также хотим, чтобы пользователь был не моложе 18 и не старше 34 лет.
Это всего лишь личный выбор, отражающий реальную реализацию. Давайте сделаем это с помощью приведенного ниже кода:
const months = [ 'January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December' ]; const startYear = 1990; const endYear = 2007;
В приведенном выше коде мы смогли задать базовые данные для приложения для выбора даты. Массив месяцев содержит все 12 месяцев.
В startYear в endYear определить асаи â Ã â Ã â Ã â Ã â Ã â Ã â Ã â Ã â Ã â Ã â Ã â Ã â Ã â Ã â Ã â Ã â Ã â Ã â Ã â Ã â Ã â Ã â Ã â Ã â Ã â Ã â Ã â Ã â Ã â Ã â Ã â Ã â Ã â Ã â Ã â Ã â Ã â Ã â Ã â Ã â Ã â Ã â Ã â Ã â Ã â Ã â Ã' Â Ã Â Ã Â Ã Â Ã Â Ã Â Ã Â Ã Â Ã Â Ã Â Ã Â Ã Â Ã Â Ã Â Ã Â Ã Â Ã Â Ã Â Ã Â Ã Â Ã Â Ã Â Ã Â Ã Â Ã Â Ã Â Ã Â Ã Â Ã Â Ã Â Ã Â Ã Â Ã Â Ã Â Ã Â Ã Â Ã Â Ã Â Ã Â Ã Â Ã Â Ã Â Ã Â Ã Â Ã Â Ã Â Ã Â Ã Â Ã Â Ã Â Ã' Â Ã Â Ã Â Ã Â Ã Â Ã Â Ã Â Ã Â Ã Â Ã Â Ã Â Ã Â Ã Â Ã Â Ã Â Ã Â Ã Â Ã Â Ã Â Ã Â Ã Â Ã Â Ã Â Ã Â Ã Â Ã Â Ã Â Ã Â Ã Â Ã Â Ã Â Ã Â Ã Â Ã Â Ã Â Ã Â Ã Â Ã Â Ã Â Ã Â Ã Â Ã Â Ã Â Ã Â Ã Â Ã Â Ã Â Ã Â Ã Â Ã Â Ã' Â Ã Â Ã Â Ã Â Ã Â Ã Â Ã Â Ã Â Ã Â Ã Â Ã Â Ã Â Ã Â Ã Â Ã Â Ã Â Ã Â Ã Â Ã Â Ã Â Ã Â Ã Â Ã Â Ã Â Ã Â Ã Â Ã Â Ã Â Ã Â Ã Â Ã Â Ã Â Ã Â Ã Â Ã Â Ã Â Ã Â Ã Â Ã Â Ã Â Ã Â Ã Â Ã Â Ã Â Ã Â Ã Â Ã Â Ã Â Ã Â Ã Â Ã'Â Ã â Ã â Ã â Ã â Ã â Ã â Ã â Ã â Ã â Ã â Ã â Ã â Ã â Ã â Ã â Ã â Ã â Ã â Ã â варьируются от белого пользователи могут выбрать свой диапазон. Мы будем использовать эти константы для заполнения выпадающих списков и проверки правильности выбора даты.
Выбор опции
Когда пользователь нажимает на любую опцию (например, "Март" или "2014"), мы хотим обновить состояние .select-display до выбранных параметров:
function selectOption(option, container) { const display = container.querySelector('.select-display'); display.textContent = option.textContent; document.querySelectorAll(`#${container.id} .option-item`).forEach(opt => opt.classList.remove('selected')); option.classList.add('selected'); updateSelectedDate(); }
В приведенном выше коде после обновления .select-display класс .selected теперь добавляется к вашему выбору, что запускает анимацию масштабирования.
При этом код очищается путем удаления выбранного класса из всех ранее выбранных параметров. В конце выполнения функции вызывается новая функция updateSelectedDate().
Эта функция будет создана ниже. Все, что она делает, - это обновляет выбранную дату в нижней части экрана, как показано в демонстрации выше.
Создание параметров
Функция CreateOptions используется для создания всех полей параметров, которые вы видите:
function createOptions(container, items, type) { const selector = container.querySelector('.options-selector'); const display = container.querySelector('.select-display'); items.forEach(item => { const option = document.createElement('div'); option.className = 'option-item'; option.textContent = item; option.addEventListener('click', () => { selectOption(option, container); container.classList.remove('open'); }); selector.appendChild(option); }); }
Описанная выше функция принимает три параметра: контейнер, элементы и тип.
Эти параметры определяют:
контейнер... Какой выпадающий список следует заполнить (выбрать месяцы, дни, годы)
элементы... Отображаемые значения (месяцы, дни 1-31, 1990-2025 годы)
введите идентификатор в раскрывающемся списке (месяц, день, год)
Функция берет эти данные и преобразует их в доступные для просмотра параметры внутри каждого выпадающего списка. Она создает новый div с классом option-item, который ранее был стилизован в CSS выше. Она также настраивает базовый обработчик щелчка, который выбирает параметр и закрывает выпадающий список.
Эта функция работает с описанной ниже функцией initializeSelectors(), которая при вызове требует отображения даты. Итак, давайте продолжим и создадим функцию initializeSelectors():
function initializeSelectors() { createOptions(monthSelect, months, 'month'); createOptions(daySelect, Array.from({length: 31}, (_, i) => i + 1), 'day'); createOptions(yearSelect, Array.from({length: endYear - startYear + 1}, (_, i) => startYear + i), 'year'); }
Приведенный выше код создает параметры месяца, используя наш массив месяцев. Затем он генерирует параметры дня от одного до 31.
Существует много способов увеличить число, но я нашел этот трюк с массивом привлекательным. Он просто создает массив с 31 пустым ячейком. Второй аргумент принимает каждый индекс, который равен i (начиная с 0), и увеличивает его на единицу. То же самое происходит и с годами. Интересно, правда?
В конце CreateOptions() и InitializeSelector() преобразуют это:
<div class="options-selector"></div>
- в прокручиваемый список опций, которые наследуют наши стили CSS с помощью функции мгновенной прокрутки.
До этого момента мы могли создавать и обрабатывать наши варианты. Теперь давайте сделаем еще один шаг вперед, выбрав выпадающий список.
Выпадающий переключатель
Функция setupDropdownHandlers() переключает выпадающие списки, по которым выполняется щелчок, с помощью открытого класса:
function setupDropdownHandlers() { document.querySelectorAll('.custom-select').forEach(select => { const display = select.querySelector('.select-display'); display.addEventListener('click', (e) => { e.stopPropagation(); // Close all other dropdowns document.querySelectorAll('.custom-select').forEach(s => { if (s !== select) s.classList.remove('open'); }); select.classList.toggle('open'); }); }); }
Это связано с вашим CSS, где .custom-select.open запускает видимость выпадающего списка через:
.custom-select.open .options-selector { opacity: 1; visibility: visible; transform: translateY(0); }
Это также предотвращает влияние щелчка на другие элементы (остановка распространения). Для удобства пользователя все другие открытые выпадающие списки закрываются путем удаления их открытого класса.
Теперь, когда мы нажимаем на месяц, год или день, у нас появляется выпадающий список с возможностью прокрутки, в котором мы можем выбрать один из наших вариантов:
Если вы заметили, что, когда мы щелкнули "Снаружи", выпадающий список не закрылся. Это важно для удобства пользователей. Мы немедленно исправим это.:
// Close dropdowns when clicking outside function clickHandler() { document.addEventListener('click', () => { document.querySelectorAll('.custom-select').forEach(select => select.classList.remove('open')); }); // Prevent closing when clicking inside dropdown document.querySelectorAll('.options-selector').forEach(selector => { selector.addEventListener('click', (e) => e.stopPropagation()); }); }
Функция clickHandler() позволяет закрывать выпадающий список при нажатии снаружи. Я сделал эту функцию достаточно умной, чтобы раскрывающиеся списки оставались открытыми при нажатии внутри них:
Вы также заметите, что указанная ниже дата не обновляется, когда мы выбираем дату:
Давайте быстро исправим это, чтобы он обновлялся синонимично всякий раз, когда вызывается функция selectOption(). Мы создадим функцию, которую мы вызвали в selectOption() выше:
function updateSelectedDate() { const month = monthSelect.querySelector('.select-display').textContent; const day = daySelect.querySelector('.select-display').textContent; const year = yearSelect.querySelector('.select-display').textContent; if (month !== 'Month' && day !== 'Day' && year !== 'Year') { selectedDate.textContent = `Selected: ${month} ${day}, ${year}`; } }
Теперь мы видим, что дата обновляется после того, как мы сделали выбор. Кроме того, вы заметите, что при прокрутке ничего не происходит. Для выбора даты вам нужно щелкнуть по опции, которая ничем не отличается от обычной формы выбора. Чтобы исправить это, мы будем использовать Intersection Observer API.
Использование API-интерфейса Intersection Observer
Мы создадим функцию под названием setupIntersectionObservers(). Именно с помощью этой функции мы создаем нашу самую важную логику прокрутки для выбора формы:
function setupIntersectionObservers() { document.querySelectorAll('.options-selector').forEach(selector => { const container = selector.closest('.custom-select'); const observer = new IntersectionObserver( (entries) => { entries.forEach(entry => { if (entry.isIntersecting) { const option = entry.target; selectOption(option, container); } }); }, { root: selector, rootMargin: '-5% 0px -94% 0px', threshold: 0 } ); selector.querySelectorAll('.option-item').forEach(option => observer.observe(option)); }); }
В приведенном выше коде мы использовали API Intersection Observer, который помогает нам следить за каждым элементом, когда он входит в определенную область окна просмотра и выходит из нее.
В нашем коде мы создали наблюдателя для каждого выпадающего списка опций в контейнере. Этот наблюдатель настроен с определенными полями (rootMargin: '-5% 0px -94% 0px'), которые создают область обнаружения в верхней части выпадающего списка.
Когда параметр прокручивается в этой области, значение isIntersecting становится равным true, вызывая выбор этого параметра. Это создает эффект привязки при прокрутке параметров.
Каждый элемент параметра (день, месяц или год) отслеживается индивидуально с помощью observer.observe(опция). Когда параметр попадает в область, вызывается функция selectOption() (которая была объявлена выше) для обновления отображения и поддержания выбранного состояния.
Наблюдатель постоянно отслеживает положение прокрутки, делая выбор более плавным и естественным, когда пользователи прокручивают параметры даты.
Это напрямую связано с поведением привязки прокрутки CSS, определенным ранее; они работают вместе, создавая безупречный эффект прокрутки. Отрицательные поля в rootMargin гарантируют, что одновременно может быть выбран только один вариант, предотвращая одновременный выбор нескольких вариантов.
Вот как теперь выглядит наше окно выбора с прокруткой:
Вывод
Это было долгое чтение, но поверьте мне, я постарался изложить его как можно короче. Даже если для вас это было немного сложно, по крайней мере, вы пополнили свои прежние знания о работе с selects. Теперь, когда у вас есть эти дополнительные сведения, вы готовы внедрить в свой проект окно выбора с прокруткой.
Для получения дополнительной информации ознакомьтесь с нашими статьями о событиях привязки прокрутки JavaScript и создании пользовательского выпадающего списка <select> с помощью CSS.
Большое спасибо вам за то, что не теряете времени даром; вот код для этой статьи. Продолжайте писать!