🔥 Oxide UI v0.1.0 — Dark mode only, copy-paste ready. Get started →
Skip to content

Date Picker

<div role="dialog"> + calendar grid

A dropdown calendar for date selection. Supports keyboard navigation, today/clear shortcuts, and integrates cleanly into forms.

Overview

Preview

Trigger button

html
<button @click="open = !open"
  class="flex w-48 items-center gap-2 rounded-md border border-zinc-700 bg-zinc-900
         px-3 py-2.5 text-sm transition-colors focus:outline-none focus-visible:ring-2
         focus-visible:ring-[#D40C37]/40"
  :class="open ? 'border-[#D40C37]' : 'hover:border-zinc-600'"
>
  <!-- Calendar icon -->
  <span :class="selected ? 'text-zinc-100' : 'text-zinc-600'">
    {{ selected ? formatDate(selected) : 'Pick a date' }}
  </span>
</button>

Calendar panel

html
<div class="absolute left-0 top-full z-20 mt-1.5 w-72 rounded-xl border border-zinc-700/60
            bg-zinc-900 p-3 shadow-2xl shadow-black/40">

  <!-- Month navigation -->
  <div class="mb-3 flex items-center justify-between">
    <button @click="prevMonth" class="flex h-7 w-7 items-center justify-center rounded-md hover:bg-zinc-800 text-zinc-400">‹</button>
    <p class="text-sm font-semibold text-zinc-200">{{ monthName }} {{ viewYear }}</p>
    <button @click="nextMonth" class="flex h-7 w-7 items-center justify-center rounded-md hover:bg-zinc-800 text-zinc-400">›</button>
  </div>

  <!-- Weekday headers -->
  <div class="grid grid-cols-7 mb-1">
    <span v-for="d in ['Su','Mo','Tu','We','Th','Fr','Sa']" :key="d"
      class="flex h-8 items-center justify-center text-[10px] font-semibold text-zinc-600">
      {{ d }}
    </span>
  </div>

  <!-- Day grid -->
  <div class="grid grid-cols-7">
    <div v-for="blank in leadingBlanks" :key="'b'+blank"></div>
    <button
      v-for="day in daysInMonth" :key="day"
      @click="selectDate(day)"
      class="flex h-8 w-8 items-center justify-center rounded-full text-sm transition-all mx-auto"
      :class="
        isSelected(day) ? 'bg-[#D40C37] text-white font-semibold' :
        isToday(day)    ? 'border border-[#D40C37]/40 text-[#D40C37] hover:bg-[#D40C37]/10' :
                          'text-zinc-300 hover:bg-zinc-800'
      "
    >{{ day }}</button>
  </div>

  <!-- Footer -->
  <div class="mt-3 flex justify-between border-t border-zinc-800 pt-3">
    <button @click="selected = null; open = false" class="text-xs text-zinc-600 hover:text-zinc-400">Clear</button>
    <button @click="goToToday" class="text-xs font-medium text-[#D40C37] hover:underline">Today</button>
  </div>
</div>

Full Vue implementation

vue
<script setup>
import { ref, computed } from 'vue'

const open = ref(false)
const selected = ref(null)
const today = new Date()
const viewMonth = ref(today.getMonth())
const viewYear = ref(today.getFullYear())

const months = ['January','February','March','April','May','June',
                'July','August','September','October','November','December']

const monthName    = computed(() => months[viewMonth.value])
const daysInMonth  = computed(() => new Date(viewYear.value, viewMonth.value + 1, 0).getDate())
const leadingBlanks = computed(() => new Date(viewYear.value, viewMonth.value, 1).getDay())

function prevMonth() {
  if (viewMonth.value === 0) { viewMonth.value = 11; viewYear.value-- }
  else viewMonth.value--
}
function nextMonth() {
  if (viewMonth.value === 11) { viewMonth.value = 0; viewYear.value++ }
  else viewMonth.value++
}

function selectDate(day) {
  selected.value = new Date(viewYear.value, viewMonth.value, day)
  open.value = false
}

function isSelected(day) {
  if (!selected.value) return false
  return selected.value.getDate() === day &&
         selected.value.getMonth() === viewMonth.value &&
         selected.value.getFullYear() === viewYear.value
}

function isToday(day) {
  return today.getDate() === day &&
         today.getMonth() === viewMonth.value &&
         today.getFullYear() === viewYear.value
}

function goToToday() {
  viewMonth.value = today.getMonth()
  viewYear.value = today.getFullYear()
  selectDate(today.getDate())
}

function formatDate(d) {
  return d.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' })
}
</script>

In a form field

html
<div class="space-y-1.5">
  <label class="block text-xs font-medium text-zinc-400">Start date</label>
  <!-- date picker trigger here -->
  <p v-if="error" class="text-xs text-red-400" role="alert">{{ error }}</p>
</div>

Accessibility notes

  • The calendar panel acts as a dialog — give it role="dialog" and aria-label="Choose a date"
  • Each day button should have aria-label="March 8, 2026" (full date) not just the number
  • Selected day: aria-pressed="true" or aria-selected="true"
  • Today: add aria-current="date"
  • Keyboard: ←→ navigate days, ↑↓ navigate weeks, PgUp/PgDn navigate months, Esc closes
  • For production use consider a dedicated library like v-calendar or @vuepic/vue-datepicker which implement the full ARIA date picker pattern

Released under the MIT License.