Skip to content

<CoarWeekView> — Week View Preview

7-day time-grid view — one hour-axis on the left, seven day columns on the right, all-day band pinned under the day-of-week header. The week's first day is locale-aware (en-US / ja-JP start on Sunday, de-AT / fr-FR start on Monday) but can be overridden via firstDayOfWeek().

html
<CoarWeekView :builder="builder" />

Standalone usage

ts
import { ref } from 'vue';
import { Temporal } from '@js-temporal/polyfill';
import {
  CoarWeekView,
  useWeekView,
  type CalendarEvent,
} from '@cocoar/vue-calendar';

const events = ref<CalendarEvent[]>([
  {
    id: 'standup',
    start: Temporal.ZonedDateTime.from('2026-04-15T09:00:00[UTC]'),
    end:   Temporal.ZonedDateTime.from('2026-04-15T09:30:00[UTC]'),
  },
]);
const date = ref('2026-04-15');

const { builder, api } = useWeekView();
builder
  .events(events)
  .date(date)
  .timezone('UTC')
  .timeRange([7, 20])
  .slotDuration(30)
  .firstDayOfWeek(1);   // Monday — overrides locale default
html
<CoarWeekView :builder="builder" />

The Week view shares its builder type, CalendarBuilder, with the Day view — they differ only in the days array the wrapper computes (Week uses weekDates(date, fdow) to expand the cursor to the 7-day window).

Mon, Apr 13
Tue, Apr 14
Wed, Apr 15
Thu, Apr 16
Fri, Apr 17
Sat, Apr 18
Sun, Apr 19
DevConf — Vienna
Sven — OOO
12 AM
1 AM
2 AM
3 AM
4 AM
5 AM
6 AM
7 AM
8 AM
9 AM
10 AM
11 AM
12 PM
1 PM
2 PM
3 PM
4 PM
5 PM
6 PM
7 PM
8 PM
9 PM
10 PM
11 PM
12 AM
Daily standup
Daily standup
Daily standup
Design review
Lunch with Anna
1:1 with Bernhard
Daily standup
Deep work — Calendar
Daily standup
Client demo
vue
<template>
  <div style="height: 600px; border: 1px solid var(--coar-border-neutral-tertiary); border-radius: var(--coar-radius-xs); overflow: hidden;">
    <CoarWeekView :builder="builder" />
  </div>
</template>

<script setup lang="ts">
import { ref } from 'vue';
import {
  CoarWeekView,
  useWeekView,
  Temporal,
  type CalendarEvent,
} from '@cocoar/vue-calendar';

const date = ref(Temporal.PlainDate.from('2026-04-15'));

const pd = (iso: string) => Temporal.PlainDate.from(iso);
const zdt = (iso: string, tz = 'Europe/Vienna') =>
  Temporal.ZonedDateTime.from(`${iso}[${tz}]`);

const events = ref<CalendarEvent[]>([
  // Multi-day all-day band entries.
  {
    id: 'devconf',
    start: pd('2026-04-13'),
    end: pd('2026-04-16'),
    meta: { title: 'DevConf — Vienna', color: '#7c3aed' },
  },
  {
    id: 'sven-ooo',
    start: pd('2026-04-15'),
    end: pd('2026-04-18'),
    meta: { title: 'Sven — OOO', color: '#9ca3af' },
  },
  // Daily standups Mon–Fri.
  ...['2026-04-13', '2026-04-14', '2026-04-15', '2026-04-16', '2026-04-17'].map(
    (d): CalendarEvent => ({
      id: `standup-${d}`,
      start: zdt(`${d}T09:00:00`),
      end: zdt(`${d}T09:30:00`),
      meta: { title: 'Daily standup', color: '#10b981' },
    }),
  ),
  // Wed busy-day cluster.
  {
    id: 'wed-design',
    start: zdt('2026-04-15T11:00:00'),
    end: zdt('2026-04-15T12:30:00'),
    meta: { title: 'Design review', color: '#8b5cf6' },
  },
  {
    id: 'wed-lunch',
    start: zdt('2026-04-15T12:00:00'),
    end: zdt('2026-04-15T13:00:00'),
    meta: { title: 'Lunch with Anna', color: '#ef4444' },
  },
  {
    id: 'wed-1on1',
    start: zdt('2026-04-15T15:00:00'),
    end: zdt('2026-04-15T15:45:00'),
    meta: { title: '1:1 with Bernhard', color: '#3b82f6' },
  },
  // Thu deep-work block.
  {
    id: 'thu-deep',
    start: zdt('2026-04-16T09:00:00'),
    end: zdt('2026-04-16T13:00:00'),
    meta: { title: 'Deep work — Calendar', color: '#2563eb' },
  },
  // Fri client demo.
  {
    id: 'fri-demo',
    start: zdt('2026-04-17T15:00:00'),
    end: zdt('2026-04-17T16:30:00'),
    meta: { title: 'Client demo', color: '#dc2626' },
  },
]);

const { builder } = useWeekView();
builder
  .events(events)
  .date(date)
  .timezone('Europe/Vienna')
  .firstDayOfWeek(1) // Monday — overrides the locale default
  .onEventDrop(({ event, next }) => {
    const idx = events.value.findIndex((e) => e.id === event.id);
    if (idx < 0) return;
    events.value = [
      ...events.value.slice(0, idx),
      { ...event, start: next.start, ...(next.end ? { end: next.end } : {}) },
      ...events.value.slice(idx + 1),
    ];
  });
</script>

Custom day header

The #dayHeader slot replaces the default per-column header. Receives { date, isToday, isWeekend } so you can render whatever fits your design — the example below stacks day-of-week + day-of-month with a today indicator and weekend muting.

Custom #dayHeader slot — day-of-week + day-of-month stacked, today highlighted with a blue dot, weekends muted.

MON13
TUE14
WED15
THU16
FRI17
SAT18
SUN19
8 AM
9 AM
10 AM
11 AM
12 PM
1 PM
2 PM
3 PM
4 PM
5 PM
6 PM
Standup
Standup
Standup
Sprint review
Standup
Standup
vue
<template>
  <div>
    <p class="hint">
      Custom <code>#dayHeader</code> slot — day-of-week + day-of-month
      stacked, today highlighted with a blue dot, weekends muted.
    </p>
    <div style="height: 540px; border: 1px solid var(--coar-border-neutral-tertiary); border-radius: var(--coar-radius-xs); overflow: hidden;">
      <CoarWeekView :builder="builder">
        <template #dayHeader="{ date: d, isToday, isWeekend }">
          <div class="hdr" :class="{ 'hdr--today': isToday, 'hdr--weekend': isWeekend }">
            <span class="hdr__dow">{{ formatDow(d) }}</span>
            <span class="hdr__day">
              {{ d.day }}
              <span v-if="isToday" class="hdr__dot" />
            </span>
          </div>
        </template>
      </CoarWeekView>
    </div>
  </div>
</template>

<script setup lang="ts">
import { ref } from 'vue';
import {
  CoarWeekView,
  useWeekView,
  Temporal,
  type CalendarEvent,
} from '@cocoar/vue-calendar';

const date = ref(Temporal.PlainDate.from('2026-04-15'));

const zdt = (iso: string, tz = 'Europe/Vienna') =>
  Temporal.ZonedDateTime.from(`${iso}[${tz}]`);

const events = ref<CalendarEvent[]>([
  ...['2026-04-13', '2026-04-14', '2026-04-15', '2026-04-16', '2026-04-17'].map(
    (d): CalendarEvent => ({
      id: `standup-${d}`,
      start: zdt(`${d}T09:00:00`),
      end: zdt(`${d}T09:30:00`),
      meta: { title: 'Standup', color: '#10b981' },
    }),
  ),
  {
    id: 'review',
    start: zdt('2026-04-15T14:00:00'),
    end: zdt('2026-04-15T15:00:00'),
    meta: { title: 'Sprint review', color: '#8b5cf6' },
  },
]);

const { builder } = useWeekView();
builder
  .events(events)
  .date(date)
  .timezone('Europe/Vienna')
  .firstDayOfWeek(1)
  .timeRange({ startMinutes: 8 * 60, endMinutes: 18 * 60 })
  .onEventDrop(({ event, next }) => {
    const idx = events.value.findIndex((e) => e.id === event.id);
    if (idx < 0) return;
    events.value = [
      ...events.value.slice(0, idx),
      { ...event, start: next.start, ...(next.end ? { end: next.end } : {}) },
      ...events.value.slice(idx + 1),
    ];
  });

const dowFmt = new Intl.DateTimeFormat('en-US', { weekday: 'short', timeZone: 'UTC' });
function formatDow(d: Temporal.PlainDate): string {
  return dowFmt
    .format(new Date(Date.UTC(d.year, d.month - 1, d.day)))
    .toUpperCase();
}
</script>

<style scoped>
.hint {
  margin: 0 0 12px;
  font-size: 13px;
  color: var(--coar-text-subtle, #6b7280);
}
.hint code {
  font-family: var(--coar-font-family-mono, monospace);
  font-size: 12px;
  background: var(--coar-background-neutral-tertiary, #f3f4f6);
  padding: 1px 5px;
  border-radius: 3px;
}
.hdr {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 2px;
  padding: 4px 8px;
}
.hdr__dow {
  font-size: 10px;
  font-weight: 700;
  letter-spacing: 0.06em;
  color: var(--coar-text-subtle, #6b7280);
}
.hdr__day {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font-size: 18px;
  font-weight: 600;
  color: var(--coar-text-base, #1a1c1f);
}
.hdr__dot {
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: var(--coar-color-accent, #2563eb);
}
.hdr--today .hdr__day {
  color: var(--coar-color-accent, #2563eb);
}
.hdr--weekend .hdr__day,
.hdr--weekend .hdr__dow {
  color: var(--coar-text-subtle, #9ca3af);
}
</style>

Inside <CoarCalendar>

<CoarCalendar> and <CoarWeekView> consume the SAME CalendarBuilder instance — there's no sub-builder forking. Set time-grid config directly on the composer's builder:

ts
const { builder } = useCalendar();
builder
  .timeRange({ startMinutes: 8 * 60, endMinutes: 18 * 60 })
  .slotDuration(15);

When the active view is week or day, the same builder feeds the embedded <CoarTimeGrid>. View-specific settings simply have no effect outside their view (e.g. maxEventsPerCell is a no-op in week view).

useWeekView<TMeta>()

ts
function useWeekView<TMeta>(): {
  builder: CalendarBuilder<TMeta>;
  api: CalendarApi<TMeta>;
};

Returns a fresh standalone builder + its imperative api. The builder type is the same CalendarBuilder used by <CoarCalendar>useWeekView() is a thin shorthand that pre-sets view: 'week'. The next() / prev() step is one week here, not one day.

Builder setters

Full reference: see the composer's API reference. Highlights that matter for the week view:

SetterArgumentDefaultNotes
firstDayOfWeek(d)0..6 | undefinedlocale-aware0 = Sunday … 6 = Saturday.
dayHeaderRenderer(r)DayHeaderRendererPer-day column header — typically the most-customised piece on a week view.
timeRange(r)MaybeRefOrGetter<{ startMinutes, endMinutes }>{0, 1440}Visible hour range, in minutes from midnight.
slotDuration(d)MaybeRefOrGetter<number>30Slot subdivision (minutes). Also the snap step when dragging.
pixelsPerHour(p)MaybeRefOrGetter<number>60Vertical density.
eventRenderer(r)EventRenderer<TMeta>Universal renderer. For all-day-band bars, branch on ctx.layout?.kind === 'allDayBar'; for time-grid event cards, ctx.layout?.kind === 'positioned'.

Multi-day events & the all-day band

Multi-day events that touch any visible day are split into one bar per row, each clipped to the row. Single-day all-day events appear in the same band. Cluster-aware lane sizing means a busy day doesn't unfairly narrow events on quieter days in the same week.

Imperative API

ts
interface CalendarApi<TMeta> {
  goTo(iso: string): void;
  goToToday(): void;
  next(): void;                  // ±1 week
  prev(): void;
  getVisibleRange(): ViewWindow | null;
  getVisibleEvents(): CalendarEvent<TMeta>[];
  scrollToTime(hour: number): void;
  refresh(): void;
  refreshRange(start: string, end: string): void;
  readonly loading: Readonly<Ref<boolean>>;
  readonly visibleRange: Readonly<Ref<ViewWindow | null>>;
  readonly gridReady: Readonly<Ref<boolean>>;
}

<CoarWeekView> props + slots

PropTypeDescription
builderCalendarBuilderRequired. From useWeekView() (or share the one from useCalendar()).
SlotScopePurpose
event{ event, layout }Per-event renderer. layout is the PositionedEvent (lane / startMinutes / endMinutes / clipping flags).
allDayEvent{ event, layout }All-day band renderer. layout is the AllDayBar (lane / startCol / endCol / clipping flags).
dayHeader{ date, isToday, isWeekend }Per-day column header.

Released under the Apache-2.0 License.