· Оновлено · 12 хв читання

Як створити компонент вибору дати зі скролом на JavaScript

Як спроєктувати JavaScript date scroller: безперервна навігація, вибір діапазону, доступність, обмежений DOM і робочий приклад RollDate Core.

Більшість JavaScript-компонентів для вибору дати показують один місяць календаря. Користувач відкриває пікер, натискає «назад» або «далі» і тоді обирає день.

Компонент зі скролом змінює цю модель навігації. Замість окремого екрана на кожен місяць він показує безперервну поверхню дат, якою можна рухатися дотиком, коліщатком, трекпадом, клавіатурою або явними кнопками.

Це здається дрібною зміною UI. Насправді ні. Коли скрол стає навігацією, компонент має тримати стан дат, переробку DOM, якоріння скролу, вибір діапазону, фокус, вимкнені дати й далекі стрибки — і не випускати це у публічне API.

Далі — ці рішення і робочий приклад RollDate Core.

Date scroller, date slider чи колесо?

Ці назви часто змішують різні інтерфейси:

  • Scrollable date picker зазвичай показує дні на календарній поверхні, якою можна безперервно рухатися сусідніми тижнями чи місяцями.
  • Date scroller — ширший термін: це може бути і календарна поверхня, і окремі колонки.
  • Wheel date picker зазвичай має незалежні колонки дня, місяця, року або часу.
  • Date slider може означати горизонтальну стрічку дат, числовий діапазон або той самий scroller. Термін неоднозначний — описуйте взаємодію явно.

RollDate Core поєднує безперервно навігований календар із колесами часу. Це поле форми: одна дата, кілька дат, діапазон або опційний час. Це не сітка подій і не планувальник.

Коли безперервний скрол доречний

Навігація скролом добре працює, коли:

  • користувачі часто обирають дати поруч із поточною;
  • діапазон перетинає межу місяця;
  • сусідні тижні мають лишатися видимими для порівняння;
  • інтерфейс використовують на сенсорних пристроях;
  • повторні натискання «назад/далі» переривають задачу.

Як єдиний спосіб дістатися далеких дат він поганий. Обирати день народження десятиліття тому суцільним скролом нудно. Надійний пікер також дає прямий перехід до місяця/року, текстове поле або явний стрибок.

Практичне правило просте: скрол — для близьких дат, пряме керування — для великих стрибків.

Тримайте стан дати поза DOM

Не робіть елементи .selected джерелом істини. Пікер зі скролом може переробити або прибрати вузол, який зараз показує вибрану дату.

Тримайте навігацію й вибір окремо:

const state = {
  viewDate: new Date(),
  selectedDates: []
}

DOM — лише проєкція цього стану. Перемальовування не має змінювати вибране значення, а скрол не має тихо комітити нову дату.

Таке розділення дає дві передбачувані дії:

scroll or navigate → change the visible dates
click, tap or Enter → change the selected dates

Генеруйте дати незалежно від відображення

Обчислення дат не повинні залежати від наявних елементів. Спочатку генератор місяця повертає дати:

function getMonthDays(year, month) {
  const count = new Date(year, month + 1, 0).getDate()

  return Array.from(
    { length: count },
    (_, index) => new Date(year, month, index + 1)
  )
}

Відображення — окремий крок:

function renderMonth(year, month) {
  const fragment = document.createDocumentFragment()

  for (const date of getMonthDays(year, month)) {
    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}`
}

Ці фрагменти показують архітектуру; це не методи API RollDate.

Обмежте обсяг змонтованого вмісту

Наївна реалізація додає ще один місяць, щойно користувач наближається до кінця контейнера. Це працює, доки в документі не накопичаться десятки старих місяців.

Краща модель тримає обмежене вікно:

previous segment
current segment
next segment

Коли навігація йде вперед, попередній сегмент можна прибрати, решту зсунути, а новий наступний — згенерувати. Стрічка дат лишається безперервною, а живий DOM — під контролем.

Це той самий принцип, що у віртуалізованих списках: зберігайте стан, з якого можна відтворити вміст, а монтуйте лише те, що потрібно навколо в’юпорта.

Компонент вибору дати RollDate під час переходу між місяцями
RollDate рухається сусідніми датами скролом і тримає вибір окремо від навігації.

Зберігайте позицію скролу під час переробки

Видалення вмісту вище в’юпорта змінює scrollTop. Якщо прибрана ділянка має 600 пікселів, усе нижче зсувається вгору на 600 пікселів.

Концептуально компенсуйте прибрану висоту:

const removedHeight = previousSegment.offsetHeight

previousSegment.remove()
container.scrollTop -= removedHeight

У продакшені ще треба врахувати таймінг розкладки, адаптивну ширину, різну висоту місяців, шрифти й поведінку браузера. Вимога та сама: переробка DOM не повинна давати видимий стрибок.

Той самий принцип діє, коли змінюються правила дат. Якщо структура не змінилася, оновлюйте disabled і selected на наявних кнопках днів, а не перебудовуйте весь календар.

Вибір діапазону крізь місяці

Діапазон — це стан, а не набір підсвічених вузлів:

const range = {
  start: new Date(2026, 8, 28),
  end: new Date(2026, 9, 4)
}

Кожен видимий день можна класифікувати як початок, середину або кінець діапазону. Весь діапазон не мусить лишатися змонтованим.

Нормалізуйте значення перед порівнянням календарних днів, щоб різний час не змінював результат:

function startOfDay(date) {
  return new Date(
    date.getFullYear(),
    date.getMonth(),
    date.getDate()
  )
}

function isInsideRange(date, start, end) {
  const value = startOfDay(date).getTime()

  return value >= startOfDay(start).getTime()
    && value <= startOfDay(end).getTime()
}

Явно тестуйте діапазони крізь місяці й роки. Вони виявляють припущення, які ховають приклади в межах одного місяця.

Правила доступності дат потребують чіткого пріоритету

Бронювання й звіти часто потребують більше, ніж список вимкнених днів. Можна дозволити лише вихідні, робочий графік або дати з колбека застосунку.

Передбачуваний порядок правил:

minDate / maxDate
        ↓
enabledDates allowlist
        ↓
disabledDates denylist

Фінальний denylist дає винятки: дозволити всі вихідні, а тоді закрити конкретне свято.

Зміна цих правил у рантаймі має оновлювати видимі дні на місці. Синхронізуйте CSS-клас, нативний disabled, aria-disabled, вибраний стан, активну клавіатурну дату й roving tabindex. Змінити лише колір — означає елемент, який виглядає вимкненим, але його все ще можна досягти або активувати неправильно.

Нативний скрол зазвичай правильна відправна точка

Не замінюйте скрол браузера великою реалізацією drag, якщо продукту справді не потрібна своя фізика.

Нативний скрол уже дає жести, коліщатко й трекпад, інерцію та поведінку платформи:

.date-scroller {
  overflow-y: auto;
  overscroll-behavior: contain;
  -webkit-overflow-scrolling: touch;
}

Scroll snapping допомагає, коли рядки мають зупинятися в передбачуваних позиціях, але агресивний snap дратує під час дослідження. Тестуйте на реальному сенсорному пристрої; коліщатко миші на десктопі — погана заміна.

Чому це пасує телефонам: Why Scroll-First Date Pickers Work Better on Mobile.

Доступність виживає після віртуалізації

Клітинки днів мають бути справжніми інтерактивними елементами:

<button
  type="button"
  aria-label="18 вересня 2026"
  aria-pressed="false"
>
  18
</button>

Пікеру зі скролом усе одно потрібні:

  • видимі стилі фокусу;
  • навігація стрілками;
  • Enter або пробіл для вибору;
  • чіткі стани disabled і selected;
  • поведінка Escape для попапа;
  • повернення фокусу після закриття;
  • свідома ціль фокусу, якщо віртуалізація прибирає активний елемент.

Roving tabindex лишає один день у сітці доступним через Tab, а стрілки рухають фокус усередині. Якщо оновлення доступності вимикає активний день, перенесіть активний стан на доступну дату, перш ніж синхронізувати tabindex.

Робочий приклад діапазону в RollDate Core

Встановіть пакет:

npm install @rolldate/core

Додайте поле:

<input id="booking-dates" type="text" autocomplete="off">

Потім ініціалізуйте пікер діапазону справжнім API RollDate Core:

import RollDate from '@rolldate/core'
import '@rolldate/core/styles'

const picker = new RollDate('#booking-dates', {
  selectType: 'range',
  startWeekFromMonday: true,
  minDate: '2026-09-01',
  maxDate: '2026-12-31',

  // Дозволити понеділок–п’ятниця.
  enabledDates: [
    { repeat: 'weekly', weekdays: [1, 2, 3, 4, 5] }
  ],

  // Denylist завжди перемагає allowlist.
  disabledDates: ['2026-12-25'],

  selectDate(value) {
    console.log(value)
  }
})

Правила доступності можна змінити без створення пікера заново:

picker.setEnabledDates([
  { repeat: 'weekly', weekdays: [0, 6] }
])

Інші методи рантайму тримають навігацію окремо від вибору:

picker.goToDate('2026-11-01') // навігація без вибору

const value = picker.getValue()
const visibleDate = picker.getViewDate()
const visibleMonth = picker.getViewMonth()

picker.setValue([
  new Date(2026, 10, 6),
  new Date(2026, 10, 9)
])

getValue() повертає зафіксований вибір. getViewDate() і getViewMonth() описують поточний в’юпорт. Саме це не дає перегляду тихо змінювати значення форми.

RollDate · лише будні

Спробуйте живе демо, поекспериментуйте в playground або читайте документацію Core.

Що тестувати

Як мінімум перевірте:

  • вибір діапазону крізь два місяці й два роки;
  • межі min/max;
  • порожній allowlist;
  • конфлікти allowlist і denylist;
  • тижневі й місячні повторювані правила;
  • зміну правил у рантаймі, коли дата вже вибрана;
  • клавіатуру після того, як активний день став недоступним;
  • закриття Escape і повернення фокусу;
  • великі стрибки навігації;
  • якоріння скролу під час переробки сегментів;
  • вузькі контейнери на десктопі й реальні сенсорні пристрої.

Ці тести перевіряють модель взаємодії, а не лише форматування дат.

Висновки

JavaScript-компонент вибору дати зі скролом — це не просто календар у overflow: auto.

Стійка архітектура будується з кількох розділень:

  • стан вибору незалежний від змонтованих вузлів;
  • генерація дат незалежна від створення DOM;
  • видимі дати незалежні від зафіксованих значень;
  • безперервна навігація незалежна від необмеженого зростання DOM;
  • візуально вимкнений стан збігається з нативним і ARIA;
  • публічні методи дат не розкривають внутрішню віртуалізацію.

Коли ці межі ясні, скрол лишається плавним, діапазони перетинають межі рендерингу, а реалізацію можна розвивати, не ламаючи API застосунків.