Skip to content

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.

Wednesday, June 10
All day
00:00
01:00
02:00
03:00
04:00
05:00
06:00
07:00
08:00
09:00
10:00
11:00
12:00
13:00
14:00
15:00
16:00
17:00
18:00
19:00
20:00
21:00
22:00
23:00

Required icons

angle-left
angle-right

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

vue
<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.

Monday, June 8
All day
00:00
01:00
02:00
03:00
04:00
05:00
06:00
07:00
08:00
09:00
10:00
11:00
12:00
13:00
14:00
15:00
16:00
17:00
18:00
19:00
20:00
21:00
22:00
23:00

vue
<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.

8 Mon
9 Tue
10 Wed
11 Thu
12 Fri
13 Sat
14 Sun
All day
07:00
08:00
09:00
10:00
11:00
12:00
13:00
14:00
15:00
16:00
17:00
18:00
19:00

vue
<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.

Thursday, June 11
All day
08:00
09:00
10:00
11:00
12:00
13:00
14:00
15:00
16:00
17:00

vue
<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.

Wednesday, June 10
All day
00:00
01:00
02:00
03:00
04:00
05:00
06:00
07:00
08:00
09:00
10:00
11:00
12:00
13:00
14:00
15:00
16:00
17:00
18:00
19:00
20:00
21:00
22:00
23:00

vue
<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).

Thursday, June 11
All day
08:00
09:00
10:00
11:00
12:00
13:00
14:00
15:00
16:00
17:00

vue
<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.

Wednesday, June 10
All day
00:00
01:00
02:00
03:00
04:00
05:00
06:00
07:00
08:00
09:00
10:00
11:00
12:00
13:00
14:00
15:00
16:00
17:00
18:00
19:00
20:00
21:00
22:00
23:00

vue
<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.

Wednesday, June 10
All day
00:00
01:00
02:00
03:00
04:00
05:00
06:00
07:00
08:00
09:00
10:00
11:00
12:00
13:00
14:00
15:00
16:00
17:00
18:00
19:00
20:00
21:00
22:00
23:00

vue
<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.

Wednesday, June 10
All day
00:00
01:00
02:00
03:00
04:00
05:00
06:00
07:00
08:00
09:00
10:00
11:00
12:00
13:00
14:00
15:00
16:00
17:00
18:00
19:00
20:00
21:00
22:00
23:00

vue
<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>

Used components