02examples
Linked month and day views
A month overview beside a day view, both controlled by the same date — click a day on the left, it opens on the right.
Two <Kloq> instances, one useState. Click a date number in the month on
the left and the day view on the right jumps there; step the day view with
‹ › and the month follows. They share the events too, so a block moved on the
right moves on the left.
"use client";
import { useState } from "react";
import { ChevronLeft, ChevronRight } from "lucide-react";
import { formatViewTitle, Kloq, useIsClient, useKloq, type CalEvent } from "kloq";
import { Button } from "@/components/ui/button";
import { monthEvents } from "./data";
/** One bar for both instances — `useKloq()` reads whichever `<Kloq>` it is inside. */
function Bar() {
const k = useKloq();
return (
<div className="flex items-center gap-0.5 border-b px-2 py-1.5">
<Button variant="ghost" size="icon-xs" aria-label="Previous" onClick={() => k.step(-1)}>
<ChevronLeft />
</Button>
<Button variant="ghost" size="icon-xs" aria-label="Next" onClick={() => k.step(1)}>
<ChevronRight />
</Button>
<span className="ml-1 truncate font-mono text-xs" suppressHydrationWarning>
{formatViewTitle(k.view, undefined, { short: true })}
</span>
<Button variant="ghost" size="xs" className="ml-auto text-muted-foreground" disabled={k.atToday} onClick={k.goToday}>
Today
</Button>
</div>
);
}
export default function LinkedCalendarsExample() {
const [date, setDate] = useState(() => new Date());
const [events, setEvents] = useState<CalEvent[]>(() => monthEvents(new Date()));
const isClient = useIsClient();
const shared = {
date,
onDateChange: setDate,
events,
onEventsChange: setEvents,
responsive: false,
persistence: false,
designMode: false,
storage: false,
} as const;
return (
<div>
<header className="flex items-center justify-between border-b px-4 py-2 font-mono text-xs text-muted-foreground">
<span>shared date</span>
<span className="text-foreground tabular-nums">
{isClient
? date.toLocaleDateString("en-US", { weekday: "short", month: "short", day: "numeric", year: "numeric" })
: ""}
</span>
<span className="tabular-nums">{events.length} events</span>
</header>
<div className="grid lg:grid-cols-[minmax(0,1fr)_minmax(0,1.2fr)]">
<div className="h-[24rem] border-b lg:h-[28rem] lg:border-b-0 lg:border-r">
<Kloq view="month" {...shared}>
<Bar />
</Kloq>
</div>
<div className="h-[28rem]">
<Kloq view="day" {...shared}>
<Bar />
</Kloq>
</div>
</div>
</div>
);
}
import { instantAt, resolveView, type CalEvent, type EventColor } from "kloq";
interface Plan {
title: string;
/** days from today, minutes from midnight, length in minutes */
day: number;
at: number;
minutes: number;
color: EventColor;
}
const h = (hours: number, minutes = 0) => hours * 60 + minutes;
const PLAN: Plan[] = [
{ title: "Board prep", day: -2, at: h(11), minutes: 120, color: "violet" },
{ title: "Standup", day: -1, at: h(9), minutes: 15, color: "blue" },
{ title: "Standup", day: 0, at: h(9), minutes: 15, color: "blue" },
{ title: "Sprint planning", day: 0, at: h(10), minutes: 60, color: "blue" },
{ title: "Lunch with Sam", day: 0, at: h(12, 30), minutes: 60, color: "emerald" },
{ title: "Design review", day: 0, at: h(15), minutes: 45, color: "violet" },
{ title: "Standup", day: 1, at: h(9), minutes: 15, color: "blue" },
{ title: "1:1 with Priya", day: 1, at: h(9, 30), minutes: 30, color: "blue" },
{ title: "Dentist", day: 2, at: h(8), minutes: 60, color: "rose" },
{ title: "Roadmap", day: 3, at: h(14), minutes: 90, color: "amber" },
{ title: "Offsite", day: 7, at: h(9), minutes: 480, color: "emerald" },
{ title: "Release", day: 9, at: h(16), minutes: 60, color: "amber" },
{ title: "Retro", day: 10, at: h(15), minutes: 45, color: "violet" },
];
/** A month of events around today, as plain timed `CalEvent`s. */
export function monthEvents(now: Date): CalEvent[] {
const view = resolveView("month", 1, now);
return PLAN.map((p, i) => ({
id: `plan-${i}`,
title: p.title,
color: p.color,
start: instantAt(view, view.todayIndex + p.day, p.at),
end: instantAt(view, view.todayIndex + p.day, p.at + p.minutes),
}));
}
How it works
Both instances get the same date and the same onDateChange, so either one
navigating updates the other — the standard
controlled pattern, applied twice.
view is controlled as well, pinned to "month" on the left and "day" on
the right.
Clicking a date number in a month view calls the calendar's jumpToDay,
which reports onDateChange(date) and then requests onViewChange("day").
The date lands in the shared state; the view request goes nowhere because
view is controlled and no onViewChange is wired — so the month stays a
month and the day view, reading the same date, opens that day.
events / onEventsChange are shared the same way. Controlled events switch
the internal store off, so a drag on the right commits through
onEventsChange into the one array both instances render from.
responsive={false} on both. The views are pinned, so there is nothing for
the responsive collapse to request;
turning it off keeps onViewChange quiet. The month still draws its compact
dot form below 640px — that is a density choice tied to the box width, not a
view change, and it is what makes a 320px month legible.
The bar above each calendar is one component, Bar, rendered twice. It calls
useKloq() and gets the state of whichever instance it sits inside — step,
goToday, atToday and the resolved view for formatViewTitle. See
toolbar & chrome.
Take it further
Add onViewChange to the right-hand instance and let it switch between
"day" and 3; the month still only ever reports the date. Or mirror the
shared date into the URL, as in View and date in the URL.