Calendar
This component shows a calendar with four different views (month, week, two-days, day). Items can be added to the calendar by filling the default slot with FluxCalendarItem instances.
Required icons
Props
initial-date?: DateTime
The initial visible date.
Default: DateTime.now()
is-loading?: boolean
Indicates that the calendar is loading.
draggable?: boolean
When true, items with an `id` can be dragged between day-cells (and time-slots in time-grid views).
Default: false
view?: 'month' | 'week' | 'two-days' | 'day'
Force a specific view. When omitted, the view auto-collapses based on the viewport (`xl`→month, `lg`→week, `md`→two-days, smaller→day).
hour-range?: [number, number]
Inclusive-exclusive hour range shown in time-grid views.
Default: [0, 24]
pixels-per-minute?: number
Vertical scale for time-grid views. Default of 0.8 yields a 48px hour.
Default: 0.8
Emits
navigate: [DateTime, DateTime, DateTime]
Triggered when the calendar is being navigated. The first parameter is the view date, the second the start date of the visible range and the third the end date of the visible range.
reschedule: [{ id: string | number; fromDate: DateTime; toDate: DateTime }]
Triggered when an item is dropped on a different day or time-slot. Only fires when `draggable` is enabled.
resize: [{ id: string | number; fromDate: DateTime; toDate: DateTime; fromDuration: number; toDuration: number }]
Triggered when an item is resized in a time-grid view via top/bottom drag-handles. Only fires when `draggable` is enabled.
dragStart: [{ id: string | number; fromDate: DateTime }]
Triggered when a draggable item starts being dragged.
dragEnd: [{ id: string | number }]
Triggered when a drag ends, regardless of whether a drop happened.
keyboardGrab: [{ id: string | number; fromDate: DateTime }]
Triggered when an item is grabbed via keyboard (Space/Enter on a focused item).
keyboardCancel: [{ id: string | number }]
Triggered when a keyboard-grab is cancelled (Escape, view switch, etc.).
Slots
default
The calendar items that should be visible.
Snippet
<template>
<FluxCalendar :initial-date="anchorDate">
<template
v-for="event of events"
:key="event.id">
<FluxCalendarItem
:date="event.date"
:id="event.id">
<div :style="cardStyle(event.color)">
{{ event.label }}
</div>
</FluxCalendarItem>
</template>
</FluxCalendar>
</template>
<script
lang="ts"
setup>
import { FluxCalendar, FluxCalendarItem } from '@flux-ui/components';
import { DateTime } from 'luxon';
type Color = 'primary' | 'success' | 'warning' | 'info';
const anchorDate = DateTime.now().startOf('month').plus({days: 9});
const events = [
{id: 1, date: anchorDate, label: 'Stand-up', color: 'primary' as Color},
{id: 2, date: anchorDate, label: 'Design review', color: 'info' as Color},
{id: 3, date: anchorDate.plus({days: 1}), label: 'Sprint planning', color: 'warning' as Color},
{id: 4, date: anchorDate.plus({days: 2}), label: 'Demo', color: 'success' as Color},
{id: 5, date: anchorDate.plus({days: 4}), label: 'Retro', color: 'primary' as Color},
{id: 6, date: anchorDate.plus({days: 7}), label: 'Release', color: 'success' as Color}
];
function cardStyle(color: Color): string {
return `padding: 6px 9px; background: var(--${color}-100); color: var(--${color}-800); border-radius: var(--radius-half); font-size: 13px; line-height: 1; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;`;
}
</script>Auto-responsive views
When you don't pass a view prop, the calendar picks the most appropriate view based on the viewport. On large screens it shows month, on medium screens it falls back to week, then two-days and finally day on small viewports.
<template>
<FluxCalendar :initial-date="anchorDate">
<template
v-for="event of events"
:key="event.id">
<FluxCalendarItem
:date="event.date"
:duration="event.duration"
:id="event.id">
<div :style="cardStyle(event.color)">
{{ event.label }}
</div>
</FluxCalendarItem>
</template>
</FluxCalendar>
</template>
<script
lang="ts"
setup>
import { FluxCalendar, FluxCalendarItem } from '@flux-ui/components';
import { DateTime } from 'luxon';
type Color = 'primary' | 'success' | 'warning' | 'info';
const anchorDate = DateTime.now().startOf('week').plus({hours: 9});
const events = [
{id: 1, date: anchorDate, duration: 30, label: 'Stand-up', color: 'primary' as Color},
{id: 2, date: anchorDate.plus({day: 1, hours: 1}), duration: 60, label: 'Design review', color: 'info' as Color},
{id: 3, date: anchorDate.plus({day: 2, hours: 4}), duration: 30, label: 'Demo', color: 'success' as Color},
{id: 4, date: anchorDate.plus({day: 3, hours: 2}), duration: 90, label: 'Workshop', color: 'warning' as Color},
{id: 5, date: anchorDate.plus({day: 4, hours: 5}), duration: 60, label: 'Retro', color: 'primary' as Color}
];
function cardStyle(color: Color): string {
return `padding: 4px 8px; background: var(--${color}-100); color: var(--${color}-800); border-radius: var(--radius-half); font-size: 12px; line-height: 1.2;`;
}
</script>Week view
A 7-column time-grid with a sticky day-header, an all-day section and a vertically scrollable hour-grid. Use duration on items to set their length in minutes.
<template>
<FluxCalendar
:initial-date="anchorDate"
:hour-range="[7, 20]"
view="week">
<template
v-for="event of events"
:key="event.id">
<FluxCalendarItem
:date="event.date"
:duration="event.duration"
:id="event.id">
<div :style="cardStyle(event.color)">
{{ event.label }}
</div>
</FluxCalendarItem>
</template>
</FluxCalendar>
</template>
<script
lang="ts"
setup>
import { FluxCalendar, FluxCalendarItem } from '@flux-ui/components';
import { DateTime } from 'luxon';
type Color = 'primary' | 'success' | 'warning' | 'info';
const anchorDate = DateTime.now().startOf('week').plus({hours: 9});
const events = [
{id: 1, date: anchorDate, duration: 30, label: 'Stand-up', color: 'primary' as Color},
{id: 2, date: anchorDate.plus({hours: 3}), duration: 60, label: 'Lunch', color: 'warning' as Color},
{id: 3, date: anchorDate.plus({day: 1, hours: 1}), duration: 120, label: 'Design review', color: 'info' as Color},
{id: 4, date: anchorDate.plus({day: 2, hours: 5}), duration: 30, label: 'Demo', color: 'success' as Color},
{id: 5, date: anchorDate.plus({day: 3, hours: 2}), duration: 90, label: 'Workshop', color: 'primary' as Color},
{id: 6, date: anchorDate.plus({day: 4, hours: 4}), duration: 60, label: 'Retro', color: 'info' as Color}
];
function cardStyle(color: Color): string {
return `padding: 4px 8px; background: var(--${color}-100); color: var(--${color}-800); border-radius: var(--radius-half); font-size: 12px; line-height: 1.2; height: 100%; box-sizing: border-box;`;
}
</script>Day view
The single-day variant of the time-grid. Combine duration with all-day for a richer day-planning view.
<template>
<FluxCalendar
:initial-date="today"
:hour-range="[8, 18]"
view="day">
<template
v-for="event of events"
:key="event.id">
<FluxCalendarItem
:date="event.date"
:duration="event.duration"
:all-day="event.allDay"
:id="event.id">
<div :style="cardStyle(event.color)">
{{ event.label }}
</div>
</FluxCalendarItem>
</template>
</FluxCalendar>
</template>
<script
lang="ts"
setup>
import { FluxCalendar, FluxCalendarItem } from '@flux-ui/components';
import { DateTime } from 'luxon';
type Color = 'primary' | 'success' | 'warning' | 'info';
const today = DateTime.now().startOf('day');
const events = [
{id: 1, date: today, allDay: true, duration: 0, label: 'On call', color: 'warning' as Color},
{id: 2, date: today.plus({hours: 9}), allDay: false, duration: 30, label: 'Stand-up', color: 'primary' as Color},
{id: 3, date: today.plus({hours: 10}), allDay: false, duration: 90, label: 'Design review', color: 'info' as Color},
{id: 4, date: today.plus({hours: 13}), allDay: false, duration: 60, label: 'Lunch', color: 'success' as Color},
{id: 5, date: today.plus({hours: 15}), allDay: false, duration: 60, label: 'Pair session', color: 'primary' as Color},
{id: 6, date: today.plus({hours: 16, minutes: 30}), allDay: false, duration: 30, label: 'Wrap up', color: 'info' as Color}
];
function cardStyle(color: Color): string {
return `padding: 4px 8px; background: var(--${color}-100); color: var(--${color}-800); border-radius: var(--radius-half); font-size: 12px; line-height: 1.2; height: 100%; box-sizing: border-box;`;
}
</script>Draggable items
Set draggable on the calendar to let users move items between day-cells (and time-slots in time-grid views). The calendar is fully controlled — listen for the reschedule event and update your own state. While dragging, hovering the previous/next month buttons advances the view so items can be moved across months.
<template>
<FluxCalendar
:initial-date="anchorDate"
:draggable="true"
@reschedule="onReschedule">
<template
v-for="event of events"
:key="event.id">
<FluxCalendarItem
:date="event.date"
:id="event.id">
<div :style="cardStyle(event.color)">
{{ event.label }}
</div>
</FluxCalendarItem>
</template>
</FluxCalendar>
</template>
<script
lang="ts"
setup>
import { FluxCalendar, FluxCalendarItem } from '@flux-ui/components';
import { DateTime } from 'luxon';
import { ref } from 'vue';
type Color = 'primary' | 'success' | 'warning' | 'info';
type Event = {
readonly id: number;
date: DateTime;
readonly label: string;
readonly color: Color;
};
const anchorDate = DateTime.now().startOf('month').plus({days: 9});
const events = ref<Event[]>([
{id: 1, date: anchorDate, label: 'Stand-up', color: 'primary'},
{id: 2, date: anchorDate.plus({days: 1}), label: 'Design review', color: 'info'},
{id: 3, date: anchorDate.plus({days: 2}), label: 'Sprint demo', color: 'success'},
{id: 4, date: anchorDate.plus({days: 3}), label: 'Retrospective', color: 'warning'},
{id: 5, date: anchorDate.plus({days: 4}), label: 'Release', color: 'success'},
{id: 6, date: anchorDate.plus({days: 7}), label: 'Planning', color: 'primary'}
]);
function onReschedule({id, toDate}: {id: number | string; toDate: DateTime}): void {
const event = events.value.find(e => e.id === id);
if (event) {
event.date = toDate;
}
}
function cardStyle(color: Color): string {
return `padding: 6px 9px; background: var(--${color}-100); color: var(--${color}-800); border-radius: var(--radius-half); font-size: 13px; line-height: 1; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;`;
}
</script>Resize
In time-grid views, items expose top and bottom drag-handles when the calendar is draggable. Listen for the resize event to update your duration (and optionally date for top-handle resizes).
<template>
<FluxCalendar
:initial-date="anchorDate"
:hour-range="[8, 18]"
view="day"
draggable
@reschedule="onReschedule"
@resize="onResize">
<template
v-for="event of events"
:key="event.id">
<FluxCalendarItem
:date="event.date"
:duration="event.duration"
:id="event.id">
<div :style="cardStyle(event.color)">
{{ event.label }} · {{ event.duration }}m
</div>
</FluxCalendarItem>
</template>
</FluxCalendar>
</template>
<script
lang="ts"
setup>
import { FluxCalendar, FluxCalendarItem } from '@flux-ui/components';
import { DateTime } from 'luxon';
import { ref } from 'vue';
type Color = 'primary' | 'success' | 'info';
type Event = {
readonly id: number;
date: DateTime;
duration: number;
readonly label: string;
readonly color: Color;
};
const anchorDate = DateTime.now().startOf('day');
const events = ref<Event[]>([
{id: 1, date: anchorDate.plus({hours: 9}), duration: 60, label: 'Stand-up', color: 'primary'},
{id: 2, date: anchorDate.plus({hours: 11}), duration: 90, label: 'Design review', color: 'info'},
{id: 3, date: anchorDate.plus({hours: 14}), duration: 30, label: 'Demo', color: 'success'}
]);
function onReschedule({id, toDate}: {id: number | string; toDate: DateTime}): void {
const event = events.value.find(e => e.id === id);
if (event) {
event.date = toDate;
}
}
function onResize({id, toDate, toDuration}: {id: number | string; toDate: DateTime; toDuration: number}): void {
const event = events.value.find(e => e.id === id);
if (event) {
event.date = toDate;
event.duration = toDuration;
}
}
function cardStyle(color: Color): string {
return `padding: 4px 8px; background: var(--${color}-100); color: var(--${color}-800); border-radius: var(--radius-half); font-size: 12px; line-height: 1.2; height: 100%; box-sizing: border-box;`;
}
</script>Keyboard navigation
When draggable is enabled, items become focusable. Press Tab to focus an item, then Space or Enter to grab. Use the arrow keys to move it (per day in month, per snap-step or per day in time-grid). Enter drops; Escape cancels.
Tab to focus an item, then use Space or Enter to grab it. Use Arrow keys to move, Enter to drop and Escape to cancel.
<template>
<div>
<p style="margin-bottom: 12px; font-size: 13px; color: var(--foreground-secondary);">
Tab to focus an item, then use <strong>Space</strong> or <strong>Enter</strong> to grab it.
Use <strong>Arrow keys</strong> to move, <strong>Enter</strong> to drop and <strong>Escape</strong> to cancel.
</p>
<FluxCalendar
:initial-date="anchorDate"
draggable
@reschedule="onReschedule">
<template
v-for="event of events"
:key="event.id">
<FluxCalendarItem
:date="event.date"
:id="event.id">
<div :style="cardStyle(event.color)">
{{ event.label }}
</div>
</FluxCalendarItem>
</template>
</FluxCalendar>
</div>
</template>
<script
lang="ts"
setup>
import { FluxCalendar, FluxCalendarItem } from '@flux-ui/components';
import { DateTime } from 'luxon';
import { ref } from 'vue';
type Color = 'primary' | 'success' | 'warning' | 'info';
type Event = {
readonly id: number;
date: DateTime;
readonly label: string;
readonly color: Color;
};
const anchorDate = DateTime.now().startOf('month').plus({days: 9});
const events = ref<Event[]>([
{id: 1, date: anchorDate, label: 'Stand-up', color: 'primary'},
{id: 2, date: anchorDate.plus({days: 1}), label: 'Design review', color: 'info'},
{id: 3, date: anchorDate.plus({days: 2}), label: 'Demo', color: 'success'},
{id: 4, date: anchorDate.plus({days: 4}), label: 'Retro', color: 'warning'},
{id: 5, date: anchorDate.plus({days: 7}), label: 'Release', color: 'success'}
]);
function onReschedule({id, toDate}: {id: number | string; toDate: DateTime}): void {
const event = events.value.find(e => e.id === id);
if (event) {
event.date = toDate;
}
}
function cardStyle(color: Color): string {
return `padding: 6px 9px; background: var(--${color}-100); color: var(--${color}-800); border-radius: var(--radius-half); font-size: 13px; line-height: 1; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;`;
}
</script>Plain items
An item without custom styling — just text in the slot. Useful for lightweight calendars where the day's events are simply listed.
<template>
<FluxCalendar :initial-date="anchorDate">
<template
v-for="event of events"
:key="event.id">
<FluxCalendarItem
:date="event.date"
:id="event.id"
@click="event.onClick">
{{ event.label }}
</FluxCalendarItem>
</template>
</FluxCalendar>
</template>
<script
lang="ts"
setup>
import { FluxCalendar, FluxCalendarItem } from '@flux-ui/components';
import { DateTime } from 'luxon';
const anchorDate = DateTime.now().startOf('month').plus({days: 9});
const events = [
{id: 1, date: anchorDate, label: 'Stand-up', onClick: undefined},
{id: 2, date: anchorDate, label: 'Lunch with team', onClick: () => alert('Lunch')},
{id: 3, date: anchorDate.plus({days: 1}), label: 'Pair programming', onClick: undefined},
{id: 4, date: anchorDate.plus({days: 2}), label: 'Code review', onClick: () => alert('Code review')},
{id: 5, date: anchorDate.plus({days: 4}), label: 'Demo', onClick: undefined},
{id: 6, date: anchorDate.plus({days: 4}), label: 'Drinks', onClick: undefined},
{id: 7, date: anchorDate.plus({days: 7}), label: 'Release', onClick: () => alert('Release')}
];
</script>Item with tooltip
Wrap your item content in a Tooltip component to surface extra detail on hover.
<template>
<FluxCalendar :initial-date="anchorDate">
<template
v-for="event of events"
:key="event.id">
<FluxCalendarItem
:date="event.date"
:id="event.id">
<FluxTooltip :content="event.tooltip">
<div :style="cardStyle(event.color)">
{{ event.label }}
</div>
</FluxTooltip>
</FluxCalendarItem>
</template>
</FluxCalendar>
</template>
<script
lang="ts"
setup>
import { FluxCalendar, FluxCalendarItem, FluxTooltip } from '@flux-ui/components';
import { DateTime } from 'luxon';
type Color = 'primary' | 'success' | 'warning';
const anchorDate = DateTime.now().startOf('month').plus({days: 9});
const events = [
{id: 1, date: anchorDate, label: 'Stand-up', tooltip: 'Daily stand-up at 09:30 with the engineering team.', color: 'primary' as Color},
{id: 2, date: anchorDate.plus({days: 1}), label: 'Lunch with Anna', tooltip: 'Lunch with Anna at the new place around the corner. 12:30.', color: 'warning' as Color},
{id: 3, date: anchorDate.plus({days: 2}), label: 'Demo', tooltip: 'Demo of the new dashboard for stakeholders. 15:00 — 16:00.', color: 'success' as Color},
{id: 4, date: anchorDate.plus({days: 4}), label: 'Retro', tooltip: 'Sprint retrospective. 14:00.', color: 'primary' as Color}
];
function cardStyle(color: Color): string {
return `padding: 6px 9px; background: var(--${color}-100); color: var(--${color}-800); border-radius: var(--radius-half); font-size: 13px; line-height: 1; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;`;
}
</script>