Component

Date Time Picker

Date Time Picker.

  • Component
  • Vanilla
  • HTML
  • MIT

Preview

Live component

Preview not available

No interactive preview for this component.

Install it with the CLI below, or read the implementation on this page.

No dependencies Responsive · Focus-visible · Keyboard-ready

Install

Add to your project

terminal
npx devsnips add Vanilla/Components/Forms/date-time-picker
registry path: Vanilla/Components/Forms/date-time-picker

Source

Implementation

code.html
<!--
  Date & Time Picker Layout
  - Native input enhancements
  - Custom calendar interface
  - Time selection with meridian
  - Keyboard accessible
  - Mobile-optimized display
-->

<div class="datetime-picker">
  <div class="input-group">
    <label for="date">Date</label>
    <div class="picker-input">
      <input 
        type="text" 
        id="date" 
        name="date" 
        readonly 
        placeholder="Select date"
        aria-haspopup="true"
        aria-expanded="false"
        aria-controls="calendar"
      >
      <button type="button" class="calendar-toggle" aria-label="Show calendar">
        <svg viewBox="0 0 24 24" width="24" height="24">
          <path d="M19 3h-1V1h-2v2H8V1H6v2H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm0 16H5V8h14v11zM7 10h5v5H7v-5z" fill="currentColor"/>
        </svg>
      </button>
    </div>
  </div>
  
  <div class="input-group">
    <label for="time">Time</label>
    <div class="picker-input">
      <input 
        type="text" 
        id="time" 
        name="time" 
        readonly 
        placeholder="Select time"
        aria-haspopup="true"
        aria-expanded="false"
        aria-controls="time-picker"
      >
      <button type="button" class="time-toggle" aria-label="Show time picker">
        <svg viewBox="0 0 24 24" width="24" height="24">
          <path d="M11.99 2C6.47 2 2 6.48 2 12s4.47 10 9.99 10C17.52 22 22 17.52 22 12S17.52 2 11.99 2zM12 20c-4.42 0-8-3.58-8-8s3.58-8 8-8 8 3.58 8 8-3.58 8-8 8zm.5-13H11v6l5.25 3.15.75-1.23-4.5-2.67V7z" fill="currentColor"/>
        </svg>
      </button>
    </div>
  </div>
  
  <div id="calendar" class="calendar" role="dialog" aria-modal="true" hidden>
    <div class="calendar-header">
      <button type="button" class="prev-month" aria-label="Previous month">
        <svg viewBox="0 0 24 24" width="24" height="24">
          <path d="M15.41 7.41L14 6l-6 6 6 6 1.41-1.41L10.83 12z" fill="currentColor"/>
        </svg>
      </button>
      <h2 class="calendar-title">September 2025</h2>
      <button type="button" class="next-month" aria-label="Next month">
        <svg viewBox="0 0 24 24" width="24" height="24">
          <path d="M10 6L8.59 7.41 13.17 12l-4.58 4.59L10 18l6-6z" fill="currentColor"/>
        </svg>
      </button>
    </div>
    
    <div class="calendar-grid">
      <div class="calendar-weekdays" role="row">
        <span role="columnheader">Su</span>
        <span role="columnheader">Mo</span>
        <span role="columnheader">Tu</span>
        <span role="columnheader">We</span>
        <span role="columnheader">Th</span>
        <span role="columnheader">Fr</span>
        <span role="columnheader">Sa</span>
      </div>
      
      <div class="calendar-days" role="grid">
        <!-- Days will be generated by JavaScript -->
      </div>
    </div>
  </div>
  
  <div id="time-picker" class="time-picker" role="dialog" aria-modal="true" hidden>
    <div class="time-columns">
      <div class="time-column" role="listbox" aria-label="Hours">
        <!-- Hours: 1-12 -->
      </div>
      <div class="time-column" role="listbox" aria-label="Minutes">
        <!-- Minutes: 00-59 -->
      </div>
      <div class="time-column" role="listbox" aria-label="Period">
        <div role="option" tabindex="0">AM</div>
        <div role="option" tabindex="-1">PM</div>
      </div>
    </div>
  </div>
</div>

<style>
.datetime-picker {
  max-width: 300px;
  margin: 2rem auto;
  padding: 0 1rem;
}

.input-group {
  margin-bottom: 1rem;
}

label {
  display: block;
  margin-bottom: 0.25rem;
  font-weight: 500;
  color: var(--ds-foreground, #333);
}

.picker-input {
  position: relative;
  display: flex;
}

input {
  width: 100%;
  padding: 0.75rem;
  padding-right: 3rem;
  border: 2px solid var(--ds-border, #ddd);
  border-radius: var(--ds-radius-sm, 4px);
  font-size: 1rem;
  cursor: pointer;
  transition: all 0.3s;
}

input:focus {
  outline: none;
  border-color: var(--ds-accent, #4a90e2);
  box-shadow: 0 0 0 2px rgba(74, 144, 226, 0.1);
}

.calendar-toggle,
.time-toggle {
  position: absolute;
  right: 0.75rem;
  top: 50%;
  transform: translateY(-50%);
  padding: 0.25rem;
  border: none;
  background: none;
  color: var(--ds-muted, #666);
  cursor: pointer;
  transition: color 0.3s;
}

.calendar-toggle:hover,
.time-toggle:hover {
  color: var(--ds-foreground, #333);
}

/* Calendar styles */
.calendar {
  position: absolute;
  top: 100%;
  left: 0;
  right: 0;
  margin-top: 0.5rem;
  padding: 1rem;
  background: white;
  border-radius: var(--ds-radius-sm, 4px);
  box-shadow: var(--ds-shadow-lg, 0 4px 12px rgba(0, 0, 0, 0.15));
  z-index: 1000;
}

.calendar-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-bottom: 1rem;
}

.calendar-title {
  margin: 0;
  font-size: 1rem;
  font-weight: 500;
}

.prev-month,
.next-month {
  padding: 0.25rem;
  border: none;
  background: none;
  color: var(--ds-muted, #666);
  cursor: pointer;
  transition: color 0.3s;
}

.prev-month:hover,
.next-month:hover {
  color: var(--ds-foreground, #333);
}

.calendar-weekdays {
  display: grid;
  grid-template-columns: repeat(7, 1fr);
  gap: 0.5rem;
  text-align: center;
  font-size: 0.875rem;
  font-weight: 500;
  color: var(--ds-muted, #666);
  margin-bottom: 0.5rem;
}

.calendar-days {
  display: grid;
  grid-template-columns: repeat(7, 1fr);
  gap: 0.5rem;
}

.calendar-day {
  aspect-ratio: 1;
  display: flex;
  align-items: center;
  justify-content: center;
  border: none;
  background: none;
  font-size: 0.875rem;
  color: var(--ds-foreground, #333);
  cursor: pointer;
  border-radius: 50%;
  transition: all 0.3s;
}

.calendar-day:hover:not(:disabled) {
  background: var(--ds-surface-2, #f5f5f5);
}

.calendar-day.today {
  font-weight: 700;
  color: var(--ds-accent, #4a90e2);
}

.calendar-day.selected {
  background: var(--ds-accent, #4a90e2);
  color: white;
}

.calendar-day:disabled {
  color: var(--ds-border-strong, #ccc);
  cursor: not-allowed;
}

/* Time picker styles */
.time-picker {
  position: absolute;
  top: 100%;
  left: 0;
  right: 0;
  margin-top: 0.5rem;
  background: white;
  border-radius: var(--ds-radius-sm, 4px);
  box-shadow: var(--ds-shadow-lg, 0 4px 12px rgba(0, 0, 0, 0.15));
  z-index: 1000;
}

.time-columns {
  display: grid;
  grid-template-columns: 2fr 2fr 1fr;
  height: 200px;
  overflow: hidden;
}

.time-column {
  overflow-y: auto;
  scrollbar-width: none;
  -ms-overflow-style: none;
}

.time-column::-webkit-scrollbar {
  display: none;
}

.time-option {
  padding: 0.5rem;
  text-align: center;
  cursor: pointer;
  transition: all 0.3s;
}

.time-option:hover {
  background: var(--ds-surface-2, #f5f5f5);
}

.time-option.selected {
  background: var(--ds-accent, #4a90e2);
  color: white;
}

/* Focus styles */
.calendar-toggle:focus-visible,
.time-toggle:focus-visible,
.prev-month:focus-visible,
.next-month:focus-visible,
.calendar-day:focus-visible,
.time-option:focus-visible {
  outline: 2px solid var(--ds-accent, #4a90e2);
  outline-offset: 2px;
  border-radius: var(--ds-radius-sm, 4px);
}

/* Mobile optimization */
@media (max-width: 600px) {
  input {
    font-size: 16px;
  }
  
  .calendar,
  .time-picker {
    position: fixed;
    top: auto;
    bottom: 0;
    left: 0;
    right: 0;
    margin: 0;
    border-radius: var(--ds-radius-lg, 12px) 12px 0 0;
    max-height: 80vh;
    overflow-y: auto;
  }
}

/* devsnips-qa:quality-bar */
@media (prefers-reduced-motion: reduce){*{animation-duration:0.01ms !important;animation-iteration-count:1 !important;transition-duration:0.01ms !important;scroll-behavior:auto !important;}}


/* DevSnips design tokens (Swiss) — edit tokens.css to re-theme */
:root{
--ds-bg:#fff;--ds-surface:#fff;--ds-surface-2:#f7f7f7;--ds-surface-3:#ededed;--ds-foreground:#0a0a0a;--ds-muted:#525252;--ds-subtle:#a3a3a3;--ds-border:#e5e5e5;--ds-border-strong:#d4d4d4;--ds-accent:#2563eb;--ds-accent-hover:#1d4ed8;--ds-accent-fg:#fff;--ds-accent-soft:#eff6ff;--ds-success:#16a34a;--ds-success-soft:#f0fdf4;--ds-warning:#d97706;--ds-warning-soft:#fffbeb;--ds-danger:#dc2626;--ds-danger-soft:#fef2f2;--ds-info:#2563eb;--ds-info-soft:#eff6ff;--ds-font-sans:system-ui,-apple-system,'Segoe UI',Roboto,Inter,'Helvetica Neue',Arial,sans-serif;--ds-font-mono:ui-monospace,'SF Mono','JetBrains Mono',Menlo,Consolas,monospace;--ds-text-xs:.75rem;--ds-text-sm:.875rem;--ds-text-base:1rem;--ds-text-lg:1.125rem;--ds-text-xl:1.25rem;--ds-text-2xl:1.5rem;--ds-text-3xl:1.875rem;--ds-text-4xl:2.25rem;--ds-leading-tight:1.25;--ds-leading-normal:1.5;--ds-leading-relaxed:1.625;--ds-weight-normal:400;--ds-weight-medium:500;--ds-weight-semibold:600;--ds-weight-bold:700;--ds-space-0:0;--ds-space-px:1px;--ds-space-1:.25rem;--ds-space-2:.5rem;--ds-space-3:.75rem;--ds-space-4:1rem;--ds-space-5:1.25rem;--ds-space-6:1.5rem;--ds-space-8:2rem;--ds-space-10:2.5rem;--ds-space-12:3rem;--ds-space-16:4rem;--ds-radius-sm:.25rem;--ds-radius-md:.5rem;--ds-radius-lg:.75rem;--ds-radius-xl:1rem;--ds-radius-2xl:1.5rem;--ds-radius-full:9999px;--ds-shadow-sm:0 1px 2px 0 rgba(0,0,0,.05);--ds-shadow-md:0 4px 6px -1px rgba(0,0,0,.08),0 2px 4px -2px rgba(0,0,0,.05);--ds-shadow-lg:0 10px 15px -3px rgba(0,0,0,.1),0 4px 6px -4px rgba(0,0,0,.05);--ds-duration-fast:120ms;--ds-duration-normal:200ms;--ds-duration-slow:300ms;--ds-ease-out:cubic-bezier(.16,1,.3,1);--ds-ease-in-out:cubic-bezier(.4,0,.2,1);--ds-ring:0 0 0 2px #2563eb;--ds-container:1200px;--ds-gutter:1rem;
}
@media (prefers-color-scheme: dark){:root{--ds-bg:#0a0a0a;--ds-surface:#111;--ds-surface-2:#171717;--ds-surface-3:#1f1f1f;--ds-foreground:#fafafa;--ds-muted:#a3a3a3;--ds-subtle:#737373;--ds-border:#262626;--ds-border-strong:#404040;--ds-accent:#3b82f6;--ds-accent-hover:#60a5fa;--ds-accent-fg:#fff;--ds-accent-soft:#172554;--ds-success:#22c55e;--ds-success-soft:#052e16;--ds-warning:#f59e0b;--ds-warning-soft:#422006;--ds-danger:#ef4444;--ds-danger-soft:#450a0a;--ds-info:#3b82f6;--ds-info-soft:#172554;--ds-ring:0 0 0 2px #3b82f6;}}
</style>

<script>
class DateTimePicker {
  constructor(container) {
    this.container = container;
    this.dateInput = container.querySelector('#date');
    this.timeInput = container.querySelector('#time');
    this.calendar = container.querySelector('#calendar');
    this.timePicker = container.querySelector('#time-picker');
    
    this.selectedDate = null;
    this.selectedTime = null;
    
    this.init();
  }
  
  init() {
    this.bindDateEvents();
    this.bindTimeEvents();
    this.generateCalendar();
    this.generateTimePicker();
    
    // Close pickers when clicking outside
    document.addEventListener('click', (e) => {
      if (!this.container.contains(e.target)) {
        this.calendar.hidden = true;
        this.timePicker.hidden = true;
        this.dateInput.setAttribute('aria-expanded', 'false');
        this.timeInput.setAttribute('aria-expanded', 'false');
      }
    });
  }
  
  bindDateEvents() {
    const calendarToggle = this.container.querySelector('.calendar-toggle');
    const prevMonth = this.container.querySelector('.prev-month');
    const nextMonth = this.container.querySelector('.next-month');
    
    [this.dateInput, calendarToggle].forEach(el => {
      el.addEventListener('click', () => {
        this.timePicker.hidden = true;
        this.calendar.hidden = !this.calendar.hidden;
        this.dateInput.setAttribute('aria-expanded', !this.calendar.hidden);
      });
    });
    
    prevMonth.addEventListener('click', () => this.changeMonth(-1));
    nextMonth.addEventListener('click', () => this.changeMonth(1));
  }
  
  bindTimeEvents() {
    const timeToggle = this.container.querySelector('.time-toggle');
    
    [this.timeInput, timeToggle].forEach(el => {
      el.addEventListener('click', () => {
        this.calendar.hidden = true;
        this.timePicker.hidden = !this.timePicker.hidden;
        this.timeInput.setAttribute('aria-expanded', !this.timePicker.hidden);
      });
    });
  }
  
  generateCalendar(date = new Date()) {
    const year = date.getFullYear();
    const month = date.getMonth();
    
    const firstDay = new Date(year, month, 1);
    const lastDay = new Date(year, month + 1, 0);
    const startPadding = firstDay.getDay();
    const daysInMonth = lastDay.getDate();
    
    this.container.querySelector('.calendar-title').textContent = 
      date.toLocaleString('default', { month: 'long', year: 'numeric' });
    
    const daysContainer = this.container.querySelector('.calendar-days');
    daysContainer.innerHTML = '';
    
    // Add padding days
    for (let i = 0; i < startPadding; i++) {
      const padDay = document.createElement('button');
      padDay.className = 'calendar-day';
      padDay.disabled = true;
      daysContainer.appendChild(padDay);
    }
    
    // Add month days
    for (let i = 1; i <= daysInMonth; i++) {
      const dayButton = document.createElement('button');
      dayButton.className = 'calendar-day';
      dayButton.textContent = i;
      dayButton.type = 'button';
      dayButton.setAttribute('role', 'gridcell');
      
      const currentDate = new Date(year, month, i);
      
      // Mark today
      if (this.isToday(currentDate)) {
        dayButton.classList.add('today');
      }
      
      // Mark selected date
      if (this.isSelectedDate(currentDate)) {
        dayButton.classList.add('selected');
      }
      
      dayButton.addEventListener('click', () => this.selectDate(currentDate));
      
      daysContainer.appendChild(dayButton);
    }
  }
  
  generateTimePicker() {
    const hoursColumn = this.timePicker.querySelector('.time-column:nth-child(1)');
    const minutesColumn = this.timePicker.querySelector('.time-column:nth-child(2)');
    
    // Generate hours (1-12)
    for (let i = 1; i <= 12; i++) {
      const hour = document.createElement('div');
      hour.className = 'time-option';
      hour.textContent = i.toString().padStart(2, '0');
      hour.setAttribute('role', 'option');
      hour.setAttribute('tabindex', '0');
      
      hour.addEventListener('click', () => this.selectHour(i));
      
      hoursColumn.appendChild(hour);
    }
    
    // Generate minutes (00-59)
    for (let i = 0; i < 60; i++) {
      const minute = document.createElement('div');
      minute.className = 'time-option';
      minute.textContent = i.toString().padStart(2, '0');
      minute.setAttribute('role', 'option');
      minute.setAttribute('tabindex', '0');
      
      minute.addEventListener('click', () => this.selectMinute(i));
      
      minutesColumn.appendChild(minute);
    }
    
    // Handle AM/PM selection
    const periodOptions = this.timePicker.querySelectorAll('.time-column:nth-child(3) [role="option"]');
    periodOptions.forEach(option => {
      option.addEventListener('click', () => {
        periodOptions.forEach(opt => {
          opt.classList.remove('selected');
          opt.setAttribute('tabindex', '-1');
        });
        option.classList.add('selected');
        option.setAttribute('tabindex', '0');
        this.updateTimeValue();
      });
    });
  }
  
  changeMonth(delta) {
    const currentDate = this.selectedDate || new Date();
    const newDate = new Date(currentDate.getFullYear(), currentDate.getMonth() + delta, 1);
    this.generateCalendar(newDate);
  }
  
  selectDate(date) {
    this.selectedDate = date;
    this.dateInput.value = date.toLocaleDateString();
    this.calendar.hidden = true;
    this.dateInput.setAttribute('aria-expanded', 'false');
    
    // Update calendar display
    const days = this.container.querySelectorAll('.calendar-day');
    days.forEach(day => day.classList.remove('selected'));
    days[date.getDate() + date.getDay() - 1].classList.add('selected');
  }
  
  selectHour(hour) {
    const hours = this.timePicker.querySelectorAll('.time-column:nth-child(1) .time-option');
    hours.forEach(h => h.classList.remove('selected'));
    hours[hour - 1].classList.add('selected');
    this.updateTimeValue();
  }
  
  selectMinute(minute) {
    const minutes = this.timePicker.querySelectorAll('.time-column:nth-child(2) .time-option');
    minutes.forEach(m => m.classList.remove('selected'));
    minutes[minute].classList.add('selected');
    this.updateTimeValue();
  }
  
  updateTimeValue() {
    const selectedHour = this.timePicker.querySelector('.time-column:nth-child(1) .time-option.selected');
    const selectedMinute = this.timePicker.querySelector('.time-column:nth-child(2) .time-option.selected');
    const selectedPeriod = this.timePicker.querySelector('.time-column:nth-child(3) .time-option.selected');
    
    if (selectedHour && selectedMinute && selectedPeriod) {
      this.timeInput.value = `${selectedHour.textContent}:${selectedMinute.textContent} ${selectedPeriod.textContent}`;
    }
  }
  
  isToday(date) {
    const today = new Date();
    return date.getDate() === today.getDate() &&
           date.getMonth() === today.getMonth() &&
           date.getFullYear() === today.getFullYear();
  }
  
  isSelectedDate(date) {
    if (!this.selectedDate) return false;
    return date.getDate() === this.selectedDate.getDate() &&
           date.getMonth() === this.selectedDate.getMonth() &&
           date.getFullYear() === this.selectedDate.getFullYear();
  }
}

// Initialize date time picker
new DateTimePicker(document.querySelector('.datetime-picker'));
</script>
555 lines UTF-8 · LF · Spaces: 2

AI-ready

Available to your agent.

This component is reachable through the DevSnips CLI, MCP server and Skills integrations — one registry, one resource path.

Resource path

devsnips://components/forms/date-time-picker

Continue browsing

View all