component — date & time

Week View

Seven days on one time axis — or five for the work week — with overlapping meetings packed into columns per day, an all-day band with "+N more" overflow, and the now-marker on today. The week starts where the locale says it does. With isEventEditable and onEventChange an event can be dragged — or arrow-keyed — to another time or another day, snapped to the slot grid; the view reports the drop and your state decides.

  • calendar
  • week
  • agenda
  • events
  • schedule
  • outlook

08 examples

Default

Seven days on one axis. The week starts where the locale says it does, and today's column carries the now-marker.

Mo5
Di6
Mi7
Do8
Fr9
Sa10
So11
09 Uhr
10 Uhr
11 Uhr
12 Uhr
13 Uhr
14 Uhr
15 Uhr
16 Uhr
17 Uhr
18 Uhr
19 Uhr

Work week

visibleDays={5} cuts the week to its working days. The chevrons still step a whole week, so the view cannot drift onto a Saturday.

My calendarTeamOps
Mo5
Di6
Mi7
Do8
Fr9
09 Uhr
10 Uhr
11 Uhr
12 Uhr
13 Uhr
14 Uhr
15 Uhr
16 Uhr
17 Uhr
18 Uhr

All-day band and overflow

Multi-day events stack into lanes above the grid. With more lanes than maxAllDayLanes allows, the last row becomes a "+N more" — and a band that would not fit on every day it covers is folded away whole rather than cut short.

Mo5
Di6
Mi7
Do8
Fr9
Sa10
So11
all day
09 Uhr
10 Uhr
11 Uhr
12 Uhr
13 Uhr

Click handler

onEventClick fires on every activation and receives the event object you passed in, so you can open your own detail panel from it.

Mo5
Di6
Mi7
Do8
Fr9
Sa10
So11
10 Uhr
11 Uhr
12 Uhr
13 Uhr
14 Uhr
15 Uhr
16 Uhr
17 Uhr

Click an event — onEventClick hands you the whole event.

Jumping to another week

The heading is a picker, and it has been the default since the alternative was Today or one chevron press at a time. It opens the unit the heading is spelled in — a week grid, where the row is the target — because every day of a row leads to the same view. pickerGranularity="day" asks for a day instead, and labelVariant="static" turns the heading back into text.

Mo5
Di6
Mi7
Do8
Fr9
Sa10
So11
09 Uhr
10 Uhr
11 Uhr
12 Uhr
13 Uhr
14 Uhr
15 Uhr
16 Uhr
17 Uhr

The heading names a week, so the grid it opens offers weeks: the row is the target and the ISO number is in the gutter.

Where the legend goes

Placement is layout, so the legend takes no prop for it: it is a child, and a class puts it before the view, after it, or in a column beside it. Over the grid is the one case that needs the library — variant="overlay" draws the same row on an elevated surface, because a bare row of small muted text with events behind it is no longer a legend.

My calendarTeamOps
Mo5
Di6
Mi7
Do8
Fr9
Sa10
So11
09 Uhr
10 Uhr
11 Uhr
12 Uhr
13 Uhr
14 Uhr
15 Uhr
16 Uhr
17 Uhr

Overlay in the corner the week leaves free

The same overlay in the other corner, with the hour axis and a column of events behind it. The surface is translucent and blurred, so the key stays readable without hiding what it covers — and which corner it takes is yours, one absolute class.

Mo5
Di6
Mi7
Do8
Fr9
Sa10
So11
10 Uhr
11 Uhr
12 Uhr
13 Uhr
14 Uhr
15 Uhr
16 Uhr
My calendarTeamOps

Drag to move

isEventEditable says which events may be picked up and onEventChange reports where one was dropped, snapped to the slot grid and never outside the hours the axis draws. The arrow keys do the same thing from the keyboard — a slot up or down, a day left or right — and the ghost shows where the event is about to land. Your state decides whether it actually lands there.

Mo5
Di6
Mi7
Do8
Fr9
Sa10
So11
09 Uhr
10 Uhr
11 Uhr
12 Uhr
13 Uhr
14 Uhr
15 Uhr
16 Uhr
17 Uhr
18 Uhr
19 Uhr
Press the arrow keys to move this event.

Drag an event to another time or another day — or tab to one and use the arrow keys.

source

week-view.tsx

Copy this into your project. Resolve its dependencies from the registryDependencies in the component's API entry.

"use client"

import type { CalendarDate } from "@internationalized/date"
import {
  type CalendarEvent,
  type CalendarGridViewProps,
  CalendarShell,
  DEFAULT_CALENDAR_TIME_ZONE,
} from "@/components/calendar-shell"
import {
  calendarRangeLabel,
  type CalendarToolbarLabelVariant,
  CalendarToolbar,
  type CalendarViewName,
  type CalendarViewOption,
  useCalendarLocale,
  useCalendarNavigation,
} from "@/components/calendar-toolbar"
import { weekRange } from "@/lib/calendar"
import { cn } from "@/lib/utils"

/**
 * WeekView — quebi design system
 *
 * Seven days on one time axis, with overlapping meetings packed into side-by-side
 * columns per day, an all-day band that folds into "+N more" for calendars that
 * have all-day events, and the now-marker on today's column. `visibleDays={5}`
 * is the work week; the week still starts where the locale says it does, so the
 * five days are Monday to Friday under `de-DE` and Sunday to Thursday under
 * `en-US` unless `firstDayOfWeek` says otherwise.
 *
 * The toolbar's heading is a week picker: the label names a week, so the grid
 * it opens offers whole weeks rather than asking which of the seven days you
 * meant when all seven lead to the same view.
 *
 * Display, selection, and — given `isEventEditable` and `onEventChange` —
 * dragging an event to another time or another day, by pointer or by arrow key.
 * The machinery for that is `CalendarShell`'s; see the note there, including
 * what it will and will not let you drop. This file decides which seven days,
 * and nothing else.
 */
export interface WeekViewProps<E extends CalendarEvent = CalendarEvent>
  extends CalendarGridViewProps<E> {
  /** Any day in the week on show. Controlled. */
  date?: CalendarDate
  /** Any day in the initial week. Defaults to today; pin it on a prerendered route. */
  defaultDate?: CalendarDate
  onDateChange?: (date: CalendarDate) => void
  /** How many days from the start of the week. 5 is the work week. Default 7. */
  visibleDays?: number
  /** Override the locale's first day — 0 is Sunday. */
  firstDayOfWeek?: "sun" | "mon" | "tue" | "wed" | "thu" | "fri" | "sat"
  showToolbar?: boolean
  view?: CalendarViewName
  views?: readonly CalendarViewOption[]
  onViewChange?: (view: CalendarViewName) => void
  label?: React.ReactNode
  /** The toolbar's date label as a picker, or as plain text. Default "picker". */
  labelVariant?: CalendarToolbarLabelVariant
  /**
   * Which grid that picker opens. Default "week".
   *
   * The heading names a week, so a week is what it offers — the row is the
   * target and the ISO number is in the gutter. `day` is there for a view whose
   * reader is really choosing a day and looking at its week around it.
   */
  pickerGranularity?: "day" | "week"
}

export function WeekView<E extends CalendarEvent = CalendarEvent>({
  date,
  defaultDate,
  onDateChange,
  visibleDays = 7,
  firstDayOfWeek,
  showToolbar = true,
  view,
  views,
  onViewChange,
  label,
  labelVariant = "picker",
  pickerGranularity = "week",
  timeZone = DEFAULT_CALENDAR_TIME_ZONE,
  locale: localeProp,
  className,
  ...shell
}: WeekViewProps<E>) {
  const locale = useCalendarLocale(localeProp)
  const navigation = useCalendarNavigation({
    date,
    defaultDate,
    onDateChange,
    // A week steps by a week even when only five days are shown: a work-week
    // view that advanced five days at a time would drift onto a Saturday.
    step: { weeks: 1 },
    timeZone,
  })
  const days = weekRange(navigation.date, locale, visibleDays, firstDayOfWeek)

  return (
    <div data-slot="week-view" className={cn("flex w-full flex-col gap-3", className)}>
      {showToolbar ? (
        <CalendarToolbar
          label={label ?? calendarRangeLabel(days, { locale, timeZone })}
          labelVariant={labelVariant}
          pickerGranularity={pickerGranularity}
          locale={locale}
          firstDayOfWeek={firstDayOfWeek}
          date={navigation.date}
          onDateChange={navigation.goTo}
          view={view}
          views={views}
          onViewChange={onViewChange}
          onPrevious={navigation.goToPrevious}
          onNext={navigation.goToNext}
          onToday={navigation.goToToday}
        />
      ) : null}
      <CalendarShell {...shell} days={days} locale={locale} timeZone={timeZone} />
    </div>
  )
}