Як зробити компонент вибору дати з прокручуванням на JavaScript
Більшість JavaScript-компонентів для вибору дат досі відкривають сітку календаря, шукають місяць і просять клікнути день. Компонент із прокручуванням змінює цю модель навігації — і рішення, які з неї випливають.
На комп’ютері це працює добре. На сенсорних пристроях природніша взаємодія часто саме прокручування.
Компонент вибору дати з прокручуванням підходить до задачі інакше. Замість окремого екрана на кожен місяць він дає рухатися датами безперервно — дотиком, коліщатком миші або трекпадом.
Зібрати його — це більше, ніж покласти календар у overflow: auto. Потрібно думати про навігацію,
рендер, стан дат, діапазони, дотик, доступність і скільки DOM ви тримаєте живим.
Ця стаття розбирає ці рішення й використовує RollDate як практичний приклад JavaScript-компонента для вибору дат із навігацією прокручуванням.
Що таке компонент вибору дати з прокручуванням?
Традиційний компонент вибору дати зазвичай працює так:
Open picker
↓
Current month
↓
Previous / Next
↓
Select date
Компонент із прокручуванням змінює модель навігації:
Open picker
↓
Continuous date surface
↓
Scroll through dates
↓
Select date
Різниця виглядає невеликою, але змінює те, як варто проєктувати компонент.
Прокручування стає частиною навігації, а не лише способом рухатися всередині контейнера.
Це особливо корисно, коли:
- користувачі часто обирають дати поруч із поточною;
- інтерфейс використовують на телефонах або планшетах;
- потрібно дивитися сусідні тижні чи місяці;
- діапазони дат перетинають межі місяців;
- не хочеться знову й знову натискати previous і next.
Це не означає, що кнопки навігації мають зникнути. Прокручування й явні контроли можуть існувати разом.
Почніть із моделі дати, а не з UI
Типова помилка під час розробки компонента вибору дати — зробити DOM джерелом істини.
Наприклад:
document.querySelector('.selected')
може сказати, яка дата зараз виглядає вибраною.
Це працює, доки календар не перемалюється.
Натомість тримайте стан вибору окремо:
const state = {
selectedDate: null,
visibleDate: new Date()
}
Для вибору діапазону:
const state = {
rangeStart: null,
rangeEnd: null,
visibleDate: new Date()
}
Намальований календар має бути відображенням цього стану, а не самим станом.
Це особливо важливо в компоненті з прокручуванням, бо дати можуть з’являтися в DOM і зникати з нього під час навігації.
Генеруйте дати незалежно від рендеру
Обчислення дат теж варто відокремити від створення DOM.
Простий генератор днів місяця може виглядати так:
function getMonthDays(year, month) {
const days = []
const count = new Date(year, month + 1, 0).getDate()
for (let day = 1; day <= count; day++) {
days.push(new Date(year, month, day))
}
return days
}
Далі ці дати можна малювати як завгодно:
function renderMonth(year, month) {
const days = getMonthDays(year, month)
const fragment = document.createDocumentFragment()
for (const date of days) {
const button = document.createElement('button')
button.type = 'button'
button.dataset.date = toDateKey(date)
button.textContent = date.getDate()
fragment.appendChild(button)
}
return fragment
}
Стабільний ключ дати корисний:
function toDateKey(date) {
const year = date.getFullYear()
const month = String(date.getMonth() + 1).padStart(2, '0')
const day = String(date.getDate()).padStart(2, '0')
return `${year}-${month}-${day}`
}
Не використовуйте рядки у форматі локалі як внутрішні ідентифікатори.
Позицію прокручування треба зберігати
Видалення контенту над в’юпортом створює іншу проблему.
Припустімо, попередній місяць займає 600 пікселів.
Якщо просто прибрати його:
previousMonth.remove()
контент нижче підстрибне вгору на 600 пікселів.
Для користувача календар стрибає.
Натомість виміряйте видалену ділянку й компенсуйте її:
const height = previousMonth.offsetHeight
previousMonth.remove()
container.scrollTop -= height
Реальні реалізації мають враховувати таймінг розкладки, різну висоту, адаптивні зміни й поведінку браузера, але принцип простий:
Переробка DOM не повинна змінювати сприйняту позицію прокручування.
Якщо користувач помічає віртуалізацію, реалізація, ймовірно, ще не завершена.
Прокручування — це навігація, а не вибір
Прокручування зазвичай має змінювати те, які дати видно.
Вона не повинна тихо змінювати вибрану дату.
Тримайте поняття окремо:
state.visibleDate
state.selectedDate
Тоді поведінка передбачувана:
scroll → navigate click/tap → select
Так само легко підтримувати явні контроли:
picker.next()
picker.prev()
picker.setDate(new Date())
Компонент з навігацією прокручуванням не зобов’язаний обмежуватися лише прокручуванням.
Вибір діапазону через кілька місяців
Діапазон стає цікавим, коли початок і кінець живуть у різних намальованих сегментах.
Сам стан лишається простим:
{
start: new Date(2026, 8, 28),
end: new Date(2026, 9, 4)
}
Рендер вирішує, чи кожна видима дата входить у діапазон:
function isInsideRange(date, start, end) {
return date >= start && date <= end
}
Але порівняння сирих об’єктів Date може здивувати, якщо відрізняється час.
Спочатку нормалізуйте календарні дати:
function startOfDay(date) {
return new Date(
date.getFullYear(),
date.getMonth(),
date.getDate()
)
}
Потім порівнюйте нормалізовані значення.
UI може розрізняти:
range start range middle range end
без потреби тримати змонтованим увесь діапазон.
Знову ж таки, вибір належить стану. DOM лише візуалізує ту частину, яка зараз видима.
Дотик і мобільна поведінка
Компонент вибору дати з прокручуванням найбільше корисний, коли прокручування відчувається природним.
Не підміняйте прокручування браузера великою власною реалізацією drag, якщо вона вам насправді не потрібна.
Нативне прокручування браузера вже дає:
- жести дотику;
- ввід із трекпада;
- коліщатко миші;
- інерцію;
- платформену поведінку прокручування.
Значна частина роботи — на CSS:
.date-scroller {
overflow-y: auto;
overscroll-behavior: contain;
-webkit-overflow-scrolling: touch;
}
За бажанням можна додати scroll snapping там, де дати чи рядки мають зупинятися в передбачуваних позиціях:
.date-row {
scroll-snap-align: start;
}
Але агресивний snapping дратує під час огляду. Перевіряйте на реальному сенсорному пристрої, а не вважайте, що коліщатко миші на десктопі — достатній симулятор мобільного.
Це не так. Чому навігація прокручуванням пасує сенсорним інтерфейсам, дивіться в статті Why Scroll-First Date Pickers Work Better on Mobile.
Адаптивність має йти за контейнером
Повторно використовуваний компонент вибору дати не може вважати, що йому належить увесь в’юпорт.
Він може з’явитися всередині:
- модального вікна;
- бічної панелі;
- форми;
- панелі дашборда;
- мобільного sheet.
Тому самих media query по в’юпорту часто замало.
Де доречно, реагуйте на фактичний розмір компонента.
Наприклад:
const observer = new ResizeObserver(entries => {
const width = entries[0].contentRect.width
root.classList.toggle('is-compact', width <= 640)
})
observer.observe(root)
Тоді той самий компонент адаптується навіть коли вікно браузера широке, а сам компонент вибору дати — вузький.
Доступність лишається важливою навіть із прокручуванням
Безперервна навігація не скасовує звичайних вимог доступності компонента вибору дати.
Дати зазвичай мають бути інтерактивними елементами:
<button
type="button"
aria-label="September 6, 2026"
>
6
</button>
Вибраний стан можна позначити так:
aria-pressed="true"
або іншою доречною семантичною моделлю залежно від архітектури віджета.
Користувачам клавіатури також потрібен передбачуваний шлях крізь інтерфейс.
Важливі моменти:
- видимі стилі фокуса;
- поведінка стрілок там, де це доречно;
- Enter або Space для вибору;
- вимкнені дати;
- оголошення вибраної дати;
- розумна поведінка фокуса, коли сегменти переробляються.
Віртуалізація робить роботу з фокусом особливо важливою. Ніколи не прибирайте елемент у фокусі, не вирішивши, куди фокус має перейти.
Тримайте публічне API незалежним від прокручування
Користувачам бібліотеки не потрібно розуміти внутрішню реалізацію прокручування.
Корисне API працює датами й станом вибору:
picker.setDate(new Date(2026, 8, 6))
picker.getDate()
picker.next()
picker.prev()
Конфігурація може описувати поведінку:
{
mode: 'range',
minDate,
maxDate,
locale: 'en',
firstDayOfWeek: 1
}
а не внутрішні сегменти DOM.
Такий поділ дозволяє пізніше змінити стратегію рендеру, не ламаючи застосунки, які вже використовують компонент.
Практичний приклад із RollDate
RollDate — це open-source JavaScript-компонент для вибору дат, побудований навколо навігації прокручуванням.
Встановлення через npm:
npm install @rolldate/core
Далі імпортуйте пакет і стилі:
import RollDate from '@rolldate/core'
import '@rolldate/core/styles'
Базовий компонент можна ініціалізувати на елементі сторінки.
Окрім простого вибору дати, RollDate підтримує такі сценарії:
- вибір однієї дати;
- діапазони дат;
- кілька вибраних дат;
- дата й час;
- підсвічені дати;
- пресети діапазонів;
- локалізація;
- вбудований і спливний режими.
У бібліотеки немає залежностей під час виконання, і вона написана так, щоб лишатися незалежною від фреймворків.
Важливе, однак, не чекліст можливостей. Її дизайн з навігацією прокручуванням ілюструє архітектуру вище: навігація може відчуватися безперервною, не роблячи вибір дати залежним від зараз змонтованого DOM.
Компонент вибору дати з прокручуванням не завжди кращий
Інтерфейс з навігацією прокручуванням не є автоматично правильним вибором.
Наприклад, обирати день народження 30 років тому безперервним прокручуванням було б абсурдно.
Для далекої навігації потрібен інший механізм:
- прямий вибір року/місяця;
- date navigator;
- текстове поле;
- явні стрибки.
Так само звичайна сітка місяця може бути простішою, коли користувачі здебільшого обирають дату з невеликого відомого діапазону.
Корисний принцип дизайну тому не такий:
Замініть кожен компонент вибору дати прокручуванням.
А такий:
Використовуйте безперервне прокручування там, де часто навігують близькими датами, і давайте явну навігацію для великих стрибків.
Висновки
Хороший JavaScript-компонент для вибору дат із прокручуванням — це не просто календар із overflow: auto.
Важливі архітектурні рішення:
- тримати стан вибору окремо від DOM;
- відокремити обчислення дат від рендеру;
- обмежити кількість змонтованого календаря;
- зберігати позицію прокручування під час переробки сегментів;
- тримати навігацію й вибір незалежними;
- де можливо, використовувати нативне прокручування браузера;
- проєктувати адаптивність навколо компонента;
- зберігати клавіатуру й доступність;
- давати явну навігацію для великих стрибків по датах.
Коли ці частини розділені, навігацію прокручуванням набагато легше осмислювати.
І, що важливіше, реалізація може прокручувати довго після того, як DOM розумно перестав рости.