A fully-featured, customizable calendar component for Vue 3 with built-in Tailwind CSS support. Perfect for building scheduling applications, event calendars, and date management systems.
π Try the Vue Calendar Live Demo on GitHub Pages
- Features
- Installation
- Quick Start
- Demo
- API Reference
- Usage Examples
- Customization
- Development
- Contributing
- Roadmap
- License
- π¨ Fully customizable with Tailwind CSS
- β‘οΈ Lightweight and performant Vue 3 component
- π§© TypeScript support with full type definitions
- π± Responsive design for all screen sizes
- π Easy to integrate with any Vue 3 project
- π±οΈ Drag and drop support for event management
- π±οΈ Click to create events - Click on empty space to create new events with automatic time rounding
- π±οΈ Improved event interaction: Edit modal only opens on single click, not on drag or resize
- π Reactive event updates: Calendar updates instantly when events are added/edited
- π¨ 22 customizable event colors using Tailwind color palette
- ποΈ Multiple view modes (month, week, day)
- π Event resizing with time snapping
- π All-day event support
- π― Time-based positioning with 5-minute snap intervals
- π οΈ Plugin architecture for extensibility
- π§ͺ Comprehensive testing with Vitest (69 tests across all components)
- π¨ Enhanced visual hierarchy with improved month view styling
- π± Mobile-optimized modals with fixed headers/footers and scrollable content
npm install @kodeglot/vue-calendar date-fns date-fns-tz
yarn add @kodeglot/vue-calendar date-fns date-fns-tz
pnpm add @kodeglot/vue-calendar date-fns date-fns-tz
Add the following script tag to your HTML:
<script src="https://unpkg.com/@kodeglot/vue-calendar/dist/vue-calendar.umd.js"></script>
π Live Demo: https://kodeglot.github.io/vue-calendar/
The demo showcases all the calendar features including:
- Multiple view modes (month, week, day)
- Click to create events with interactive guides
- Custom event content and modals
- Drag and drop functionality
- Event resizing
- Custom controls and navigation
- Event creation with metadata
- Enhanced visual hierarchy with improved styling
- Mobile-optimized modals with responsive design
npm install @kodeglot/vue-calendar pinia date-fns date-fns-tz
Important: You must import the CSS file for the calendar to display correctly:
// main.ts
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'
// Import the calendar styles - THIS IS REQUIRED!
import '@kodeglot/vue-calendar/style.css'
const app = createApp(App)
app.use(createPinia())
app.mount('#app')
Alternative import path (if the above doesn't work):
import '@kodeglot/vue-calendar/dist/style.css'
Create your calendar store using Pinia:
// stores/calendarStore.ts
import { defineStore } from 'pinia'
import { ref } from 'vue'
import { v4 as uuidv4 } from 'uuid'
export interface CalendarEvent {
id: string
title: string
description?: string // Optional event description
location?: string // Optional event location
start: string // ISO date string (UTC)
end: string // ISO date string (UTC)
tailwindColor: string // Tailwind color name (e.g., 'blue', 'red', 'green')
allDay?: boolean
width?: number // For event stacking
left?: number // For event positioning
marginLeft?: number // For event spacing
order?: number // Vertical stacking order
metadata?: Record<string, any> // Custom key-value pairs for user-defined data
}
export const useCalendarStore = defineStore('calendar', () => {
const events = ref<Map<string, CalendarEvent>>(new Map())
const currentDate = ref(new Date())
const selectedDate = ref<Date | null>(null)
const addEvent = (event: CalendarEvent) => {
if (!event.id) {
event.id = uuidv4()
}
events.value.set(event.id, event)
}
const updateEventDateOnly = (eventId: string, newDate: Date) => {
const event = events.value.get(eventId)
if (event) {
const start = new Date(event.start)
const end = new Date(event.end)
const duration = end.getTime() - start.getTime()
const newStart = new Date(newDate)
const newEnd = new Date(newStart.getTime() + duration)
event.start = newStart.toISOString()
event.end = newEnd.toISOString()
events.value.set(eventId, event)
}
}
const getEventsForDate = (date: Date): CalendarEvent[] => {
return Array.from(events.value.values()).filter(event =>
new Date(event.start).toDateString() === date.toDateString()
)
}
return {
events,
currentDate,
selectedDate,
addEvent,
updateEventDateOnly,
getEventsForDate
}
})
<template>
<CalendarView
:initial-date="new Date()"
:initial-view="'month'"
:show-controls="true"
:show-event-button="true"
:enable-drag-drop="true"
@event-created="handleEventCreated"
@event-updated="handleEventUpdated"
@event-deleted="handleEventDeleted"
/>
</template>
<script setup lang="ts">
import CalendarView from '@kodeglot/vue-calendar'
const handleEventCreated = (event) => {
console.log('Event created:', event)
}
const handleEventUpdated = (event) => {
console.log('Event updated:', event)
}
const handleEventDeleted = (eventId) => {
console.log('Event deleted:', eventId)
}
</script>
## Click to Create Events
The calendar now supports creating events by clicking on empty space:
### Month View
- Click on any date cell to create an event at 9 AM on that date
- Previous/next month dates have a subtle gray background for better visual hierarchy
### Week/Day View
- Click on any time slot to create an event at that specific time
- Times are automatically rounded to the nearest 5-minute interval
- All new events default to 1-hour duration
### Features
- **Automatic time rounding**: Times are rounded to the nearest 5 minutes for consistency
- **Default duration**: New events automatically span 1 hour
- **Visual feedback**: Clear indication of clickable areas
- **Cross-view consistency**: Works seamlessly across all view modes
## Event Interaction
The calendar provides intuitive event interaction across all views:
### Creating Events
- **Month View**: Click on empty date cells to create events at 9 AM on that date
- **Week/Day View**: Click on empty time slots to create events at that specific time
- **All Views**: Use the "Create Event" button for quick event creation
### Editing Events
- **Click on any event** to open the edit modal
- **Month View**: Events are draggable between days (preserves time)
- **Week/Day View**: Events can be dragged and resized with time snapping
- **All Views**: Edit modal provides full event management (title, time, color, delete)
### Drag and Drop
- **Month View**: Drag events between days while preserving original time
- **Week/Day View**: Drag events to new times and resize for duration changes
- **Time Preservation**: Moving events between days maintains the original time components
## Debug Logging
Vue Calendar includes a comprehensive debug logging system that helps with development and troubleshooting:
### Automatic Debug Mode
- **Development**: Debug logs are automatically enabled when running `npm run dev`
- **Production**: Debug logs are disabled by default to keep builds clean
- **Conditional Output**: All debug logs are wrapped in conditional checks
### Browser Console Controls
You can control debug mode directly from the browser console:
```javascript
// Enable debug mode
__enableCalendarDebug()
// Disable debug mode
__disableCalendarDebug()
// Check current debug status
__getCalendarDebugStatus()
The debug system logs comprehensive information about:
- Calendar Lifecycle: Component mounting, updates, and cleanup
- User Interactions: Clicks, drags, resizes, and navigation
- Event Operations: Creation, updates, deletions, and movements
- Data Flow: Store updates, event filtering, and reactivity
- Performance: Timing for expensive operations
- Errors: Detailed error information with context
import { debug } from '@kodeglot/vue-calendar'
// Basic logging
debug.log('User clicked event:', eventId)
// Warning and error logging
debug.warn('Event not found:', eventId)
debug.error('Failed to update event:', error)
// Grouped logging for complex operations
debug.group('Event Drag Operation')
debug.log('Drag started:', { eventId, startTime })
debug.log('Drag position:', { x, y })
debug.groupEnd()
// Performance timing
debug.time('Event Filtering')
// ... your code ...
debug.timeEnd('Event Filtering')
[Vue Calendar Debug] Store: Adding event {eventId: "123", title: "Meeting", start: "2024-01-15T10:00:00Z"}
[Vue Calendar Debug] CalendarView: Component mounted
[Vue Calendar Debug] Event: Drag started {eventId: "123", position: {x: 100, y: 200}}
[Vue Calendar Debug] Store: Updating event date only {eventId: "123", preservedTime: "10:0:0"}
If the calendar appears unstyled or broken, make sure you've imported the CSS:
-
Check your main.ts/main.js file - ensure you have:
import '@kodeglot/vue-calendar/style.css'
-
Try the alternative import path:
import '@kodeglot/vue-calendar/dist/style.css'
-
If using a bundler, make sure it's configured to handle CSS imports
-
For CDN usage, include the CSS file:
<link rel="stylesheet" href="https://unpkg.com/@kodeglot/vue-calendar/dist/style.css">
If you're experiencing issues with:
- Missing event colors (orange, purple, etc.)
- Resize handles not visible
- Hover effects not working
- Cursor changes not appearing
This is likely a CSS purging issue in your implementing app. The calendar uses dynamic Tailwind classes that may be purged during your app's build process.
Add this comprehensive safelist to your implementing app's tailwind.config.js
:
/** @type {import('tailwindcss').Config} */
export default {
content: [
// ... your content paths
],
safelist: [
// Background colors - ALL variants
'bg-red-50', 'bg-red-100', 'bg-red-200', 'bg-red-300', 'bg-red-400', 'bg-red-500', 'bg-red-600', 'bg-red-700', 'bg-red-800', 'bg-red-900',
'bg-orange-50', 'bg-orange-100', 'bg-orange-200', 'bg-orange-300', 'bg-orange-400', 'bg-orange-500', 'bg-orange-600', 'bg-orange-700', 'bg-orange-800', 'bg-orange-900',
'bg-amber-50', 'bg-amber-100', 'bg-amber-200', 'bg-amber-300', 'bg-amber-400', 'bg-amber-500', 'bg-amber-600', 'bg-amber-700', 'bg-amber-800', 'bg-amber-900',
'bg-yellow-50', 'bg-yellow-100', 'bg-yellow-200', 'bg-yellow-300', 'bg-yellow-400', 'bg-yellow-500', 'bg-yellow-600', 'bg-yellow-700', 'bg-yellow-800', 'bg-yellow-900',
'bg-lime-50', 'bg-lime-100', 'bg-lime-200', 'bg-lime-300', 'bg-lime-400', 'bg-lime-500', 'bg-lime-600', 'bg-lime-700', 'bg-lime-800', 'bg-lime-900',
'bg-green-50', 'bg-green-100', 'bg-green-200', 'bg-green-300', 'bg-green-400', 'bg-green-500', 'bg-green-600', 'bg-green-700', 'bg-green-800', 'bg-green-900',
'bg-emerald-50', 'bg-emerald-100', 'bg-emerald-200', 'bg-emerald-300', 'bg-emerald-400', 'bg-emerald-500', 'bg-emerald-600', 'bg-emerald-700', 'bg-emerald-800', 'bg-emerald-900',
'bg-teal-50', 'bg-teal-100', 'bg-teal-200', 'bg-teal-300', 'bg-teal-400', 'bg-teal-500', 'bg-teal-600', 'bg-teal-700', 'bg-teal-800', 'bg-teal-900',
'bg-cyan-50', 'bg-cyan-100', 'bg-cyan-200', 'bg-cyan-300', 'bg-cyan-400', 'bg-cyan-500', 'bg-cyan-600', 'bg-cyan-700', 'bg-cyan-800', 'bg-cyan-900',
'bg-sky-50', 'bg-sky-100', 'bg-sky-200', 'bg-sky-300', 'bg-sky-400', 'bg-sky-500', 'bg-sky-600', 'bg-sky-700', 'bg-sky-800', 'bg-sky-900',
'bg-blue-50', 'bg-blue-100', 'bg-blue-200', 'bg-blue-300', 'bg-blue-400', 'bg-blue-500', 'bg-blue-600', 'bg-blue-700', 'bg-blue-800', 'bg-blue-900',
'bg-indigo-50', 'bg-indigo-100', 'bg-indigo-200', 'bg-indigo-300', 'bg-indigo-400', 'bg-indigo-500', 'bg-indigo-600', 'bg-indigo-700', 'bg-indigo-800', 'bg-indigo-900',
'bg-violet-50', 'bg-violet-100', 'bg-violet-200', 'bg-violet-300', 'bg-violet-400', 'bg-violet-500', 'bg-violet-600', 'bg-violet-700', 'bg-violet-800', 'bg-violet-900',
'bg-purple-50', 'bg-purple-100', 'bg-purple-200', 'bg-purple-300', 'bg-purple-400', 'bg-purple-500', 'bg-purple-600', 'bg-purple-700', 'bg-purple-800', 'bg-purple-900',
'bg-fuchsia-50', 'bg-fuchsia-100', 'bg-fuchsia-200', 'bg-fuchsia-300', 'bg-fuchsia-400', 'bg-fuchsia-500', 'bg-fuchsia-600', 'bg-fuchsia-700', 'bg-fuchsia-800', 'bg-fuchsia-900',
'bg-pink-50', 'bg-pink-100', 'bg-pink-200', 'bg-pink-300', 'bg-pink-400', 'bg-pink-500', 'bg-pink-600', 'bg-pink-700', 'bg-pink-800', 'bg-pink-900',
'bg-rose-50', 'bg-rose-100', 'bg-rose-200', 'bg-rose-300', 'bg-rose-400', 'bg-rose-500', 'bg-rose-600', 'bg-rose-700', 'bg-rose-800', 'bg-rose-900',
'bg-slate-50', 'bg-slate-100', 'bg-slate-200', 'bg-slate-300', 'bg-slate-400', 'bg-slate-500', 'bg-slate-600', 'bg-slate-700', 'bg-slate-800', 'bg-slate-900',
'bg-gray-50', 'bg-gray-100', 'bg-gray-200', 'bg-gray-300', 'bg-gray-400', 'bg-gray-500', 'bg-gray-600', 'bg-gray-700', 'bg-gray-800', 'bg-gray-900',
'bg-zinc-50', 'bg-zinc-100', 'bg-zinc-200', 'bg-zinc-300', 'bg-zinc-400', 'bg-zinc-500', 'bg-zinc-600', 'bg-zinc-700', 'bg-zinc-800', 'bg-zinc-900',
'bg-neutral-50', 'bg-neutral-100', 'bg-neutral-200', 'bg-neutral-300', 'bg-neutral-400', 'bg-neutral-500', 'bg-neutral-600', 'bg-neutral-700', 'bg-neutral-800', 'bg-neutral-900',
'bg-stone-50', 'bg-stone-100', 'bg-stone-200', 'bg-stone-300', 'bg-stone-400', 'bg-stone-500', 'bg-stone-600', 'bg-stone-700', 'bg-stone-800', 'bg-stone-900',
// Left border colors specifically
'border-l-red-50', 'border-l-red-100', 'border-l-red-200', 'border-l-red-300', 'border-l-red-400', 'border-l-red-500', 'border-l-red-600', 'border-l-red-700', 'border-l-red-800', 'border-l-red-900',
'border-l-orange-50', 'border-l-orange-100', 'border-l-orange-200', 'border-l-orange-300', 'border-l-orange-400', 'border-l-orange-500', 'border-l-orange-600', 'border-l-orange-700', 'border-l-orange-800', 'border-l-orange-900',
'border-l-amber-50', 'border-l-amber-100', 'border-l-amber-200', 'border-l-amber-300', 'border-l-amber-400', 'border-l-amber-500', 'border-l-amber-600', 'border-l-amber-700', 'border-l-amber-800', 'border-l-amber-900',
'border-l-yellow-50', 'border-l-yellow-100', 'border-l-yellow-200', 'border-l-yellow-300', 'border-l-yellow-400', 'border-l-yellow-500', 'border-l-yellow-600', 'border-l-yellow-700', 'border-l-yellow-800', 'border-l-yellow-900',
'border-l-lime-50', 'border-l-lime-100', 'border-l-lime-200', 'border-l-lime-300', 'border-l-lime-400', 'border-l-lime-500', 'border-l-lime-600', 'border-l-lime-700', 'border-l-lime-800', 'border-l-lime-900',
'border-l-green-50', 'border-l-green-100', 'border-l-green-200', 'border-l-green-300', 'border-l-green-400', 'border-l-green-500', 'border-l-green-600', 'border-l-green-700', 'border-l-green-800', 'border-l-green-900',
'border-l-emerald-50', 'border-l-emerald-100', 'border-l-emerald-200', 'border-l-emerald-300', 'border-l-emerald-400', 'border-l-emerald-500', 'border-l-emerald-600', 'border-l-emerald-700', 'border-l-emerald-800', 'border-l-emerald-900',
'border-l-teal-50', 'border-l-teal-100', 'border-l-teal-200', 'border-l-teal-300', 'border-l-teal-400', 'border-l-teal-500', 'border-l-teal-600', 'border-l-teal-700', 'border-l-teal-800', 'border-l-teal-900',
'border-l-cyan-50', 'border-l-cyan-100', 'border-l-cyan-200', 'border-l-cyan-300', 'border-l-cyan-400', 'border-l-cyan-500', 'border-l-cyan-600', 'border-l-cyan-700', 'border-l-cyan-800', 'border-l-cyan-900',
'border-l-sky-50', 'border-l-sky-100', 'border-l-sky-200', 'border-l-sky-300', 'border-l-sky-400', 'border-l-sky-500', 'border-l-sky-600', 'border-l-sky-700', 'border-l-sky-800', 'border-l-sky-900',
'border-l-blue-50', 'border-l-blue-100', 'border-l-blue-200', 'border-l-blue-300', 'border-l-blue-400', 'border-l-blue-500', 'border-l-blue-600', 'border-l-blue-700', 'border-l-blue-800', 'border-l-blue-900',
'border-l-indigo-50', 'border-l-indigo-100', 'border-l-indigo-200', 'border-l-indigo-300', 'border-l-indigo-400', 'border-l-indigo-500', 'border-l-indigo-600', 'border-l-indigo-700', 'border-l-indigo-800', 'border-l-indigo-900',
'border-l-violet-50', 'border-l-violet-100', 'border-l-violet-200', 'border-l-violet-300', 'border-l-violet-400', 'border-l-violet-500', 'border-l-violet-600', 'border-l-violet-700', 'border-l-violet-800', 'border-l-violet-900',
'border-l-purple-50', 'border-l-purple-100', 'border-l-purple-200', 'border-l-purple-300', 'border-l-purple-400', 'border-l-purple-500', 'border-l-purple-600', 'border-l-purple-700', 'border-l-purple-800', 'border-l-purple-900',
'border-l-fuchsia-50', 'border-l-fuchsia-100', 'border-l-fuchsia-200', 'border-l-fuchsia-300', 'border-l-fuchsia-400', 'border-l-fuchsia-500', 'border-l-fuchsia-600', 'border-l-fuchsia-700', 'border-l-fuchsia-800', 'border-l-fuchsia-900',
'border-l-pink-50', 'border-l-pink-100', 'border-l-pink-200', 'border-l-pink-300', 'border-l-pink-400', 'border-l-pink-500', 'border-l-pink-600', 'border-l-pink-700', 'border-l-pink-800', 'border-l-pink-900',
'border-l-rose-50', 'border-l-rose-100', 'border-l-rose-200', 'border-l-rose-300', 'border-l-rose-400', 'border-l-rose-500', 'border-l-rose-600', 'border-l-rose-700', 'border-l-rose-800', 'border-l-rose-900',
'border-l-slate-50', 'border-l-slate-100', 'border-l-slate-200', 'border-l-slate-300', 'border-l-slate-400', 'border-l-slate-500', 'border-l-slate-600', 'border-l-slate-700', 'border-l-slate-800', 'border-l-slate-900',
'border-l-gray-50', 'border-l-gray-100', 'border-l-gray-200', 'border-l-gray-300', 'border-l-gray-400', 'border-l-gray-500', 'border-l-gray-600', 'border-l-gray-700', 'border-l-gray-800', 'border-l-gray-900',
'border-l-zinc-50', 'border-l-zinc-100', 'border-l-zinc-200', 'border-l-zinc-300', 'border-l-zinc-400', 'border-l-zinc-500', 'border-l-zinc-600', 'border-l-zinc-700', 'border-l-zinc-800', 'border-l-zinc-900',
'border-l-neutral-50', 'border-l-neutral-100', 'border-l-neutral-200', 'border-l-neutral-300', 'border-l-neutral-400', 'border-l-neutral-500', 'border-l-neutral-600', 'border-l-neutral-700', 'border-l-neutral-800', 'border-l-neutral-900',
'border-l-stone-50', 'border-l-stone-100', 'border-l-stone-200', 'border-l-stone-300', 'border-l-stone-400', 'border-l-stone-500', 'border-l-stone-600', 'border-l-stone-700', 'border-l-stone-800', 'border-l-stone-900',
// Resize handle and interaction classes
'bg-gray-300', 'bg-gray-400', 'bg-gray-500',
'cursor-row-resize', 'cursor-col-resize', 'cursor-move', 'cursor-pointer',
'transition-opacity', 'transition-all', 'duration-200', 'opacity-0', 'opacity-100',
'group-hover:opacity-100',
// Common utility classes
'absolute', 'relative', 'fixed', 'sticky', 'top-0', 'bottom-0', 'left-0', 'right-0', 'inset-0',
'z-10', 'z-20', 'z-30', 'z-40', 'z-50',
'flex', 'flex-col', 'flex-row', 'flex-wrap', 'flex-nowrap', 'items-center', 'items-start', 'items-end',
'justify-center', 'justify-between', 'justify-start', 'justify-end', 'grow', 'shrink',
'grid', 'grid-cols-1', 'grid-cols-2', 'grid-cols-3', 'grid-cols-4', 'grid-cols-5', 'grid-cols-6', 'grid-cols-7',
'p-1', 'p-2', 'p-3', 'p-4', 'px-1', 'px-2', 'px-3', 'px-4', 'py-1', 'py-2', 'py-3', 'py-4',
'm-1', 'm-2', 'm-3', 'm-4', 'mb-1', 'mb-2', 'mb-3', 'mb-4', 'mt-1', 'mt-2', 'mt-3', 'mt-4',
'text-xs', 'text-sm', 'text-base', 'text-lg', 'text-xl', 'text-2xl', 'text-3xl', 'text-4xl',
'font-light', 'font-normal', 'font-medium', 'font-semibold', 'font-bold', 'font-extrabold',
'text-center', 'text-left', 'text-right', 'whitespace-nowrap', 'truncate',
'border', 'border-t', 'border-b', 'border-l', 'border-r', 'border-0', 'border-2', 'border-4',
'rounded', 'rounded-lg', 'rounded-md', 'rounded-sm', 'rounded-full',
'shadow', 'shadow-sm', 'shadow-md', 'shadow-lg', 'shadow-xl',
'opacity-0', 'opacity-10', 'opacity-20', 'opacity-30', 'opacity-40', 'opacity-50', 'opacity-60', 'opacity-70', 'opacity-80', 'opacity-90', 'opacity-100',
'transition', 'transition-all', 'transition-opacity', 'transition-colors',
'duration-75', 'duration-100', 'duration-150', 'duration-200', 'duration-300', 'duration-500', 'duration-700', 'duration-1000',
'overflow-auto', 'overflow-hidden', 'overflow-visible', 'overflow-scroll',
'min-h-screen', 'min-h-32', 'h-24', 'h-2', 'h-8', 'w-20', 'w-8', 'w-full',
'gap-px', 'gap-1', 'gap-2', 'gap-3', 'gap-4', 'gap-y-1', 'gap-y-2', 'gap-y-3', 'gap-y-4',
'hover:bg-gray-100', 'hover:bg-blue-600', 'hover:bg-blue-700', 'hover:shadow-sm',
'focus:outline-none', 'focus:ring-2', 'focus:ring-primary-500',
'group-hover:opacity-100',
'event-transition', 'bg-opacity-10', 'pointer-events-none', 'container', 'mx-auto'
],
// ... rest of your config
}
For development, you can temporarily disable CSS purging:
/** @type {import('tailwindcss').Config} */
export default {
content: [
// ... your content paths
],
// Temporarily disable purging for development
safelist: [
{
pattern: /.*/, // This will keep ALL classes (use only in development!)
}
],
// ... rest of your config
}
- "Calendar looks broken": Missing CSS import
- "Tailwind classes not working": CSS not imported or Tailwind not configured in consuming app
- "Events not visible": Check that Pinia store is properly set up
- "Missing colors": Add comprehensive safelist to your Tailwind config
- "Resize handles not working": Add cursor and opacity classes to safelist
- "Hover effects missing": Add group-hover and transition classes to safelist
Vue Calendar includes comprehensive timezone support:
- ISO Storage: All dates are stored in ISO format (UTC) for consistency
- Localized Display: Times are displayed in the user's local timezone
- Automatic Detection: User's timezone is automatically detected
- Proper Conversions: All timezone conversions are handled correctly
<script setup lang="ts">
import { useTimezone } from '@kodeglot/vue-calendar'
const {
formatTime,
formatTime12,
formatDate,
formatDateTime,
toUserTimezone,
toUTC,
userTimezone
} = useTimezone()
// Format times for display
const displayTime = formatTime('2024-01-15T10:30:00Z') // Shows in user's timezone
const displayTime12 = formatTime12('2024-01-15T10:30:00Z') // 12-hour format
// Convert between timezones
const localTime = toUserTimezone('2024-01-15T10:30:00Z')
const utcTime = toUTC(localTime)
console.log('User timezone:', userTimezone.value)
</script>
The calendar supports both 12-hour and 24-hour time formats:
<template>
<!-- 24-hour format (default) -->
<CalendarEventComponent
:event="event"
:time-format="'24h'"
/>
<!-- 12-hour format -->
<CalendarEventComponent
:event="event"
:time-format="'12h'"
/>
</template>
Vue Calendar provides multiple ways to try out the component:
# Clone the repository
git clone https://github.com/Kodeglot/vue-calendar.git
cd vue-calendar
# Install dependencies
npm install
# Build the component
npm run build
# Start the interactive demo
npm run demo
The development demo will open at http://localhost:3001
and provides:
- Hot reloading for development
- All calendar features including drag & drop
- Time format switching (12h/24h)
- Event creation and editing
- Responsive design testing
For a production-like experience without build tools:
# After building the component
npm run build
# Open the standalone demo
open demo/standalone.html
Note: The standalone demo requires the component to be built first (npm run build
).
Both demos showcase:
- Multiple Views: Month, week, and day views
- Event Management: Create, edit, and delete events
- Drag & Drop: Move events between dates and times
- Event Resizing: Resize events to change duration
- Color Customization: 22 Tailwind color options
- Time Format: Switch between 12-hour and 24-hour formats
- Responsive Design: Works on all screen sizes
- Timezone Support: Proper timezone handling
If you encounter issues with the demo:
-
"devtoolsApi is not defined": This is a harmless warning from Pinia's devtools. The demo will work normally.
-
"Cannot destructure property 'createPinia'": Make sure you've run
npm run build
before starting the demo. -
Component not loading: Check that the UMD build was created successfully in the
dist/
folder. -
Tailwind CDN warning: This is expected in development. For production, install Tailwind CSS as a PostCSS plugin.
-
File not found errors: Ensure you're running the demo from the project root directory.
Prop | Type | Default | Description |
---|---|---|---|
initialDate |
Date |
new Date() |
Initial date to display |
initialView |
'month' | 'week' | 'day' |
'month' |
Initial view mode |
timeFormat |
'12h' | '24h' |
'24h' |
Time format for displaying times |
showControls |
boolean |
true |
Show navigation controls |
showEventButton |
boolean |
true |
Show add event button |
enableDragDrop |
boolean |
true |
Enable drag and drop functionality |
customClasses |
CustomClasses |
{} |
Custom CSS classes for styling |
Event | Parameters | Description |
---|---|---|
date-change |
date: Date |
Emitted when displayed date changes |
event-created |
event: CalendarEvent |
Emitted when new event is created |
event-updated |
event: CalendarEvent |
Emitted when event is updated |
event-deleted |
eventId: string |
Emitted when event is deleted |
openEventModal |
date: Date |
Emitted when event modal is opened |
export interface CalendarEvent {
id: string
title: string
description?: string // Optional event description
location?: string // Optional event location
start: string // ISO date string (UTC)
end: string // ISO date string (UTC)
tailwindColor: string // Tailwind color name (e.g., 'blue', 'red', 'green')
allDay?: boolean
width?: number // For event stacking
left?: number // For event positioning
marginLeft?: number // For event spacing
order?: number // Vertical stacking order
metadata?: Record<string, any> // Custom key-value pairs for user-defined data
}
The calendar supports 22 Tailwind colors for events:
red
,orange
,amber
,yellow
,lime
,green
,emerald
teal
,cyan
,sky
,blue
,indigo
,violet
,purple
fuchsia
,pink
,rose
,slate
,gray
,zinc
,neutral
,stone
<template>
<div class="h-screen p-4">
<CalendarView
:initial-date="new Date()"
:initial-view="'month'"
:time-format="'24h'"
:show-controls="true"
:show-event-button="true"
:enable-drag-drop="true"
/>
</div>
</template>
<script setup lang="ts">
import { CalendarView } from '@kodeglot/vue-calendar'
</script>
<template>
<CalendarView
:initial-date="new Date()"
:initial-view="'month'"
:custom-classes="{
container: 'rounded-xl shadow-2xl border-2 border-blue-200',
header: 'bg-gradient-to-r from-blue-500 to-purple-600 text-white',
controls: 'bg-gray-50 rounded-lg p-2',
viewSelector: 'border-blue-300 focus:border-blue-500',
eventButton: 'bg-blue-600 hover:bg-blue-700 text-white font-semibold'
}"
/>
</template>
<script setup lang="ts">
import { CalendarView } from '@kodeglot/vue-calendar'
</script>
The calendar now supports fully customizable controls through slots. You can replace the default navigation, view selector, and event creation components with your own:
<template>
<CalendarView
:initial-date="new Date()"
:initial-view="'month'"
:show-controls="true"
:show-event-button="false"
>
<!-- Custom Navigation -->
<template #navigation="{ currentDate, currentView, previousPeriod, nextPeriod, headerDate }">
<div class="flex items-center gap-4">
<button
@click="previousPeriod"
class="p-2 bg-gray-100 rounded-lg hover:bg-gray-200"
>
<ChevronLeftIcon class="w-5 h-5" />
</button>
<div class="text-center">
<h2 class="text-xl font-bold">{{ headerDate }}</h2>
<p class="text-sm text-gray-500">{{ currentView }} view</p>
</div>
<button
@click="nextPeriod"
class="p-2 bg-gray-100 rounded-lg hover:bg-gray-200"
>
<ChevronRightIcon class="w-5 h-5" />
</button>
</div>
</template>
<!-- Custom View Selector -->
<template #view-selector="{ currentView, setView, currentDate }">
<div class="flex gap-1 bg-gray-100 rounded-lg p-1">
<button
@click="setView('month')"
:class="[
'px-3 py-1 rounded text-sm font-medium transition-colors',
currentView === 'month'
? 'bg-white text-blue-600 shadow-sm'
: 'text-gray-600 hover:text-gray-900'
]"
>
Month
</button>
<button
@click="setView('week')"
:class="[
'px-3 py-1 rounded text-sm font-medium transition-colors',
currentView === 'week'
? 'bg-white text-blue-600 shadow-sm'
: 'text-gray-600 hover:text-gray-900'
]"
>
Week
</button>
<button
@click="setView('day')"
:class="[
'px-3 py-1 rounded text-sm font-medium transition-colors',
currentView === 'day'
? 'bg-white text-blue-600 shadow-sm'
: 'text-gray-600 hover:text-gray-900'
]"
>
Day
</button>
</div>
</template>
<!-- Custom Event Button -->
<template #event-button="{ toggleNewEventForm, currentDate }">
<button
@click="handleCustomEventCreation"
class="bg-green-500 text-white px-4 py-2 rounded-lg hover:bg-green-600 flex items-center gap-2"
>
<PlusIcon class="w-4 h-4" />
Add Event
</button>
</template>
</CalendarView>
</template>
<script setup lang="ts">
import { CalendarView, type CalendarEvent } from '@kodeglot/vue-calendar'
import { ChevronLeftIcon, ChevronRightIcon, PlusIcon } from '@heroicons/vue/24/outline'
function handleCustomEventCreation() {
// Your custom event creation logic
console.log('Custom event creation triggered')
// You could open your own modal, navigate to a form, etc.
}
</script>
The calendar provides full TypeScript support for slot props:
import type {
NavigationSlotProps,
ControlsSlotProps,
ViewSelectorSlotProps,
EventButtonSlotProps
} from '@kodeglot/vue-calendar'
// Navigation slot props
interface NavigationSlotProps {
currentDate: Date
currentView: 'month' | 'week' | 'day'
previousPeriod: () => void
nextPeriod: () => void
headerDate: string
}
// Controls slot props
interface ControlsSlotProps {
currentDate: Date
currentView: 'month' | 'week' | 'day'
timeFormat: '12h' | '24h'
toggleNewEventForm: () => void
}
// View selector slot props
interface ViewSelectorSlotProps {
currentView: 'month' | 'week' | 'day'
currentDate: Date
}
// Event button slot props
interface EventButtonSlotProps {
toggleNewEventForm: () => void
currentDate: Date
}
You can create a minimal calendar with no default controls:
<template>
<CalendarView
:initial-date="new Date()"
:initial-view="'month'"
:show-controls="false"
:show-event-button="false"
/>
</template>
<script setup lang="ts">
import { CalendarView } from '@kodeglot/vue-calendar'
</script>
Or provide only specific custom controls while keeping others default:
<template>
<CalendarView
:initial-date="new Date()"
:initial-view="'month'"
:show-controls="true"
:show-event-button="false"
>
<!-- Only customize the event button, keep default navigation and view selector -->
<template #event-button="{ toggleNewEventForm, currentDate }">
<button
@click="openMyCustomModal"
class="bg-purple-500 text-white px-4 py-2 rounded-lg hover:bg-purple-600"
>
Create New Event
</button>
</template>
</CalendarView>
</template>
<script setup lang="ts">
import { CalendarView } from '@kodeglot/vue-calendar'
function openMyCustomModal() {
// Your custom modal logic
console.log('Opening custom modal')
}
</script>
<template>
<CalendarView
:initial-date="new Date()"
:initial-view="'week'"
@event-created="handleEventCreated"
@event-updated="handleEventUpdated"
@event-deleted="handleEventDeleted"
/>
</template>
<script setup lang="ts">
import { CalendarView, useCalendarStore, type CalendarEvent } from '@kodeglot/vue-calendar'
const store = useCalendarStore()
function handleEventCreated(event: CalendarEvent) {
// Add to your backend
store.addEvent(event)
console.log('New event:', event)
}
function handleEventUpdated(event: CalendarEvent) {
// Update in your backend
console.log('Updated event:', event)
}
function handleEventDeleted(eventId: string) {
// Delete from your backend
console.log('Deleted event:', eventId)
}
</script>
<template>
<CalendarView
:initial-date="new Date()"
:initial-view="'week'"
:enable-drag-drop="true"
@event-dropped="handleEventDrop"
/>
</template>
<script setup lang="ts">
import { CalendarView, useCalendarStore } from '@kodeglot/vue-calendar'
const store = useCalendarStore()
function handleEventDrop(eventId: string, newDate: Date) {
// Update event date in store
store.updateEventDateOnly(eventId, newDate)
// Sync with backend
console.log(`Event ${eventId} moved to ${newDate}`)
}
</script>
<template>
<div>
<CalendarMonthComponent :current-date="currentDate" />
<CalendarWeekComponent :current-date="currentDate" :hour-height="60" />
<CalendarDayComponent :current-date="currentDate" :hour-height="60" />
</div>
</template>
<script setup lang="ts">
import {
CalendarMonthComponent,
CalendarWeekComponent,
CalendarDayComponent
} from '@kodeglot/vue-calendar'
const currentDate = new Date()
</script>
<template>
<div>
<!-- 24-hour format (default) -->
<CalendarView
:initial-date="new Date()"
:initial-view="'week'"
:time-format="'24h'"
/>
<!-- 12-hour format with AM/PM -->
<CalendarView
:initial-date="new Date()"
:initial-view="'week'"
:time-format="'12h'"
/>
</div>
</template>
<script setup lang="ts">
import { CalendarView } from '@kodeglot/vue-calendar'
</script>
You can customize how each event is rendered using the #event-content
slot. The slot receives the event as a prop:
<CalendarView>
<template #event-content="{ event }">
<div>
<strong>{{ event.title }}</strong>
<span>{{ event.start }}</span>
<!-- Add any custom markup here -->
</div>
</template>
</CalendarView>
You can store and display custom data using the metadata
field:
<CalendarView>
<template #event-content="{ event }">
<div class="flex items-center gap-2">
<span class="font-bold">{{ event.title }}</span>
<!-- Show priority badge -->
<span v-if="event.metadata?.priority"
class="text-xs px-1 py-0.5 rounded"
:class="{
'bg-red-100 text-red-700': event.metadata.priority === 'high',
'bg-yellow-100 text-yellow-700': event.metadata.priority === 'medium',
'bg-green-100 text-green-700': event.metadata.priority === 'low'
}">
{{ event.metadata.priority }}
</span>
<!-- Show location -->
<span v-if="event.metadata?.location" class="text-xs text-gray-500">
π {{ event.metadata.location }}
</span>
</div>
</template>
</CalendarView>
You can provide your own modal UI for editing events using the #event-modal
slot. The slot receives the following props:
event
: The event being editedupdate
: Call this function with the updated event to save changesdelete
: Call this function with the event id to deleteclose
: Call this function to close the modal
<CalendarView>
<template #event-modal="{ event, update, delete: deleteEvent, close }">
<MyCustomEventModal
v-if="event"
:event="event"
@save="update"
@delete="deleteEvent"
@close="close"
/>
</template>
</CalendarView>
You can edit custom metadata fields in your modal:
<CalendarView>
<template #event-modal="{ event, update, delete: deleteEvent, close }">
<div v-if="event" class="modal">
<!-- Basic fields -->
<input v-model="event.title" placeholder="Event title" />
<input v-model="event.start" type="datetime-local" />
<input v-model="event.end" type="datetime-local" />
<!-- Custom metadata fields -->
<select v-model="event.metadata.priority">
<option value="">No priority</option>
<option value="low">Low</option>
<option value="medium">Medium</option>
<option value="high">High</option>
</select>
<input v-model="event.metadata.location" placeholder="Location" />
<input v-model="event.metadata.attendees" placeholder="Attendees" />
<textarea v-model="event.metadata.notes" placeholder="Notes"></textarea>
<button @click="() => update({ ...event })">Save</button>
<button @click="() => deleteEvent(event.id)">Delete</button>
<button @click="close">Cancel</button>
</div>
</template>
</CalendarView>
When creating events, you can include custom metadata:
const newEvent: CalendarEvent = {
id: crypto.randomUUID(),
title: "Team Meeting",
start: "2024-01-15T10:00:00.000Z",
end: "2024-01-15T11:00:00.000Z",
tailwindColor: "blue",
metadata: {
priority: "high",
location: "Conference Room A",
attendees: "John, Jane, Bob",
notes: "Quarterly planning discussion",
category: "work",
reminder: true
}
}
You can customize the appearance using the customClasses
prop:
interface CustomClasses {
container?: string // Main container
header?: string // Header section
controls?: string // Navigation controls
viewSelector?: string // View selector dropdown
eventButton?: string // Event creation button
}
Events can be colored using any of the 22 Tailwind colors:
const event: CalendarEvent = {
id: '1',
title: 'Meeting',
start: '2024-01-15T10:00:00Z',
end: '2024-01-15T11:00:00Z',
tailwindColor: 'blue' // Will use blue-100 background and blue-500 border
}
The calendar automatically formats times based on the user's locale and timezone settings.
Event Modal: Use the #event-modal
slot to provide your own event editing UI. The calendar will always show a fallback modal if you do not provide one.
Other Slots: You can also customize navigation, controls, and view selectors using the provided slots.
# Clone the repository
git clone https://github.com/Kodeglot/vue-calendar.git
cd vue-calendar
# Install dependencies
npm install
# Start development server
npm run dev
# Start demo server
npm run demo
# Build for production
npm run build
# Run tests
npm run test
# Run tests in watch mode
npm run test:watch
# Run test coverage
npm run test:coverage
# Type checking
npm run type-check
# Linting
npm run lint
# Test Suite Robustness
#
# - The test suite covers all major features, including timezone handling, drag and drop, resizing, and edge cases.
# - Tests use robust, implementation-agnostic assertions and mock Pinia and timezones for reliable, reproducible results.
# - Contributions should include tests for new features and edge cases.
src/
βββ components/ # Reusable components
β βββ CalendarDayComponent.vue
β βββ CalendarEventComponent.vue
β βββ CalendarMonthComponent.vue
β βββ CalendarWeekComponent.vue
β βββ EventModal.vue
β βββ TimeGridComponent.vue
βββ composables/ # Vue composables
β βββ useCalendarEventInteractions.ts
βββ stores/ # Pinia stores
β βββ calendarStore.ts
βββ views/ # Main view components
β βββ CalendarView.vue
βββ index.ts # Package entry point
βββ main.ts # App entry point
demo/
βββ index.html # Demo page
We welcome contributions! Please read our Contributing Guide for details on how to get started.
- Use TypeScript for all new code
- Follow Vue 3 Composition API patterns
- Write tests for new features
- Use Tailwind CSS for styling
- Follow the existing code structure
- Year View - Add yearly calendar view
- Resource View - Support for resource scheduling
- Timeline View - Horizontal timeline layout
- Recurring Events - Built-in recurrence rules
- Timezone Support - Enhanced timezone handling
- Export/Import - Calendar data persistence
- Event Templates - Predefined event templates
- Advanced Filtering - Event filtering and search
- Performance Optimization - Virtual scrolling for large datasets
This project is licensed under the MIT License - see the LICENSE file for details.