Live demo · Edit on StackBlitz
Horizontal infinite-scroll date rail (day / month strip) picker for Vue 3. Built for mobile-first apps — delivery tracking, booking, scheduling — where a horizontally scrollable strip of days beats a calendar popup.
- Infinite scroll — days load on demand as you scroll toward either end, with scroll-position compensation (no visual jump when past dates are prepended)
<DateRail>+<MonthRail>— day strip and month strip, sharing one theming system- Headless core — the infinite-scroll logic is exposed as a
useDateRail()composable; bring your own markup if the default cells don't fit - i18n via
Intl— pass any BCP 47 locale (ko,en,ja,de,ar, …); no locale files to import - min/max dates & disabled dates — range clamping stops the infinite scroll at the boundary
- Marker slot — render event dots/badges under any date cell
- Keyboard navigation —
←/→move a day (month),Homejumps to today - Desktop-friendly scrolling — vertical mouse wheel is translated to horizontal scroll, and mouse drag-to-scroll works out of the box (touch uses native scrolling)
- Themeable — plain CSS custom properties (
--vdr-*), no preprocessor, dark-mode friendly - Tailwind-ready —
unstyled+cellClassprops anddata-selected/data-today/data-disabledattributes for utility-CSS styling - Zero dependencies — only
vueas a peer dependency
npm install @devslab/vue-date-rail<script setup lang="ts">
import { ref } from 'vue';
import { DateRail } from '@devslab/vue-date-rail';
import '@devslab/vue-date-rail/style.css';
const date = ref(new Date());
</script>
<template>
<DateRail v-model="date" locale="ko" />
</template>Or register globally:
import { DateRailPlugin } from '@devslab/vue-date-rail';
app.use(DateRailPlugin); // registers <DateRail> and <MonthRail><DateRail
v-model="date"
locale="en"
:initial-past-days="30"
:initial-future-days="30"
:load-more-days="14"
:min-date="minDate"
:max-date="maxDate"
:disabled-date="(d) => d.getDay() === 0"
>
<template #marker="{ date }">
<span v-if="hasEvent(date)" class="dot" />
</template>
</DateRail>| Prop | Type | Default | Description |
|---|---|---|---|
modelValue |
Date |
— (required) | Selected date (v-model). Emitted normalized to local midnight. |
locale |
string |
'en' |
BCP 47 locale for Intl-based labels. |
formatters |
Partial<RailFormatters> |
— | Override individual label formatters. |
initialPastDays |
number |
30 |
Days rendered before today on init. |
initialFutureDays |
number |
30 |
Days rendered after today on init. |
loadMoreDays |
number |
14 |
Days appended per infinite-scroll load. |
minDate / maxDate |
Date |
— | Clamp the scrollable/selectable range. |
disabledDate |
(date: Date) => boolean |
— | Disable individual dates. |
autoScroll |
boolean |
true |
Auto-center the selected cell when modelValue changes. |
ariaLabel |
string |
'Select date' |
Accessible label for the rail. |
unstyled |
boolean |
false |
Skip default visual styles (structure only) for utility-CSS styling. |
cellClass |
string | (ctx) => string |
— | Extra classes per cell; the function receives { date, selected, today, disabled }. |
| Event | Payload | |
|---|---|---|
update:modelValue |
Date |
v-model update (local midnight) |
change |
Date |
Fired alongside update:modelValue |
| Slot | Props | Description |
|---|---|---|
cell |
{ date, selected, today, disabled } |
Replace the entire cell content. |
marker |
{ date } |
Rendered under the day number — event dots, badges. |
| Method | Description |
|---|---|
scrollToSelected(behavior?) |
Center the selected cell. |
scrollToToday() |
Select today (emits update:modelValue) and center it. |
reset() |
Regenerate the range around the current modelValue. |
<MonthRail
v-model="month"
locale="ko"
:past-months="12"
:future-months="12"
/>modelValue is always emitted normalized to the first of the month. If modelValue falls outside the pastMonths/futureMonths window, the range extends automatically. Slot item ({ month, selected, current }) replaces the pill content.
All the infinite-scroll mechanics without the default markup:
<script setup lang="ts">
import { onMounted } from 'vue';
import { useDateRail, dateKey } from '@devslab/vue-date-rail';
const { containerRef, dates, handleScroll, initialize } =
useDateRail({ initialPastDays: 14, loadMoreDays: 7 });
onMounted(() => initialize());
</script>
<template>
<div
ref="containerRef"
style="display: flex; overflow-x: auto"
@scroll.passive="handleScroll"
>
<button v-for="d in dates" :key="dateKey(d)" :data-vdr-key="dateKey(d)">
{{ d.getDate() }}
</button>
</div>
</template>Cells must carry
data-vdr-key="YYYY-MM-DD"(use the exporteddateKey) forscrollToDateto find them.
Override CSS custom properties on any ancestor (all have built-in fallbacks):
.my-app {
--vdr-accent: #4f82b5; /* selected / today / focus */
--vdr-accent-hover: #416d99;
--vdr-cell-bg: #f4f5f7;
--vdr-cell-hover-bg: #e8eaee;
--vdr-text: #1f2937;
--vdr-text-selected: #ffffff;
--vdr-today-bg: #eef4fa;
--vdr-radius: 12px;
--vdr-gap: 8px;
--vdr-cell-min-width: 56px;
--vdr-fs-day: 1.25rem;
--vdr-month-border: #d6d9de; /* MonthRail pill */
}Dark mode is just another set of overrides — see demo/App.vue.
Every cell exposes data-selected / data-today / data-disabled attributes, so you can style states with Tailwind's data-* variants. Combine with unstyled to drop the default look entirely:
<DateRail
v-model="date"
unstyled
cell-class="flex flex-col items-center min-w-14 rounded-xl px-2 py-2
bg-gray-100 hover:bg-gray-200
data-selected:bg-blue-600 data-selected:text-white
data-today:ring-2 data-today:ring-blue-400
data-disabled:opacity-30"
/>Or compute classes per state with a function: :cell-class="({ selected }) => selected ? '...' : '...'". <MonthRail> offers the same via unstyled + item-class and data-selected / data-current. This works the same way with UnoCSS or any attribute-variant utility framework.
- vue-month-spinner-picker — iOS-style drum-roll month picker (bottom sheet), the modal sibling of
<MonthRail> - More open source from devslab