Calendar Schedule
A working calendar schedule with illustrative data and frontend interactions.
Make it yours.
Install the editable source and its dependencies into your React + Tailwind project.
Scroll horizontally for long lines
pnpm dlx shadcn@4.0.8 add http://localhost:3000/r/calendar-schedule.jsonUse it
Scroll horizontally for long lines
"use client";
import { CalendarSchedule } from "@/components/jez-ui/blocks/calendar-schedule";
export default function Example() {
return (
<>
<CalendarSchedule />
</>
);
}
Compose your own
These styled parts are included in the same install. Use children to build your layout; add className only when you want an override.
Scroll horizontally for long lines
"use client";
import {
CalendarSchedule,
CalendarScheduleCalendar,
CalendarScheduleAgenda,
} from "@/components/jez-ui/blocks/calendar-schedule";
export default function Example() {
return (
<CalendarSchedule>
<CalendarScheduleCalendar />
<CalendarScheduleAgenda />
</CalendarSchedule>
);
}
Scroll horizontally for long lines
import { CalendarSchedule, CalendarScheduleContent, CalendarScheduleItemTitle, CalendarScheduleItem, CalendarScheduleCalendar, CalendarScheduleAgenda } from "@/components/jez-ui/blocks/calendar-schedule";Props & types
Scroll horizontally for long lines
export type CalendarScheduleOptions = {
className?: string;
events?: typeof CalendarScheduleDefaultEvents;
value?: string;
defaultValue?: string;
onValueChange?: (value: string) => void;
};
export type CalendarScheduleProps = Omit<
React.ComponentProps<"div">,
keyof CalendarScheduleOptions
> &
CalendarScheduleOptions;
CalendarSchedulePropsSource
View 5 source files
registry/blocks/calendar-schedule.tsx
Scroll horizontally for long lines
"use client";
import * as React from "react";
import { useControllable } from "../ui/use-controllable";
import { cn } from "../ui/utils";
import { Calendar } from "../ui/calendar";
export type CalendarScheduleOptions = {
className?: string;
events?: typeof CalendarScheduleDefaultEvents;
value?: string;
defaultValue?: string;
onValueChange?: (value: string) => void;
};
export type CalendarScheduleProps = Omit<
React.ComponentProps<"div">,
keyof CalendarScheduleOptions
> &
CalendarScheduleOptions;
const CalendarScheduleDefaultEvents = [
{ id: "review", date: "2026-09-08", content: "09:30 · Design review" },
{ id: "focus", date: "2026-09-08", content: "11:00 · Focus time" },
{ id: "catch-up", date: "2026-09-08", content: "14:00 · Project catch-up" },
];
function useCalendarScheduleModel({
value: suppliedValue,
defaultValue = "2026-09-08",
onValueChange,
events = CalendarScheduleDefaultEvents,
className,
children,
...rootProps
}: CalendarScheduleProps) {
const [date, setDate] = useControllable<string>(
suppliedValue,
defaultValue,
onValueChange,
);
return {
suppliedValue,
defaultValue,
onValueChange,
events,
className,
children,
rootProps,
date,
setDate,
};
}
const CalendarScheduleCompositionContext = React.createContext<ReturnType<
typeof useCalendarScheduleModel
> | null>(null);
function useCalendarScheduleComposition() {
const context = React.useContext(CalendarScheduleCompositionContext);
if (!context)
throw new Error("CalendarSchedule parts must be inside CalendarSchedule.");
return context;
}
export function CalendarSchedule(props: CalendarScheduleProps) {
const model = useCalendarScheduleModel(props);
const { className, rootProps, children } = model;
return (
<CalendarScheduleCompositionContext.Provider value={model}>
<div
{...rootProps}
className={cn(
"flex flex-wrap gap-8 rounded-xl border border-border p-5",
className,
)}
>
{children !== undefined ? (
children
) : (
<>
<CalendarScheduleCalendar />
<CalendarScheduleAgenda />
</>
)}
</div>
</CalendarScheduleCompositionContext.Provider>
);
}
export function CalendarScheduleContent({
className,
...props
}: React.ComponentProps<"section">) {
return (
<section
data-slot="calendar-schedule-content"
className={cn("min-w-48 flex-1", className)}
{...props}
/>
);
}
export function CalendarScheduleItemTitle({
className,
...props
}: React.ComponentProps<"h3">) {
return (
<h3
data-slot="calendar-schedule-itemtitle"
className={cn("mb-5 text-lg font-semibold", className)}
{...props}
/>
);
}
export function CalendarScheduleItem({
className,
...props
}: React.ComponentProps<"li">) {
return (
<li
data-slot="calendar-schedule-item"
className={cn(
"rounded-r-lg border-l-2 border-primary bg-primary/5 p-4 text-sm",
className,
)}
{...props}
/>
);
}
export function CalendarScheduleCalendar({
children,
...props
}: Partial<React.ComponentProps<typeof Calendar>> & {
children?: React.ReactNode;
}) {
const { date, setDate } = useCalendarScheduleComposition();
return (
<Calendar
value={date}
{...props}
onValueChange={(value) => {
setDate(value);
props.onValueChange?.(value);
}}
>
{children}
</Calendar>
);
}
export function CalendarScheduleAgenda({
children,
...props
}: Partial<React.ComponentProps<typeof CalendarScheduleContent>> & {
children?: React.ReactNode;
}) {
const { events, date } = useCalendarScheduleComposition();
return (
<CalendarScheduleContent {...props}>
{children === undefined ? (
<>
<p className="mb-1 text-xs text-muted-foreground">Your schedule</p>
<CalendarScheduleItemTitle>
{new Date(date + "T12:00:00").toLocaleDateString("en-GB", {
weekday: "long",
day: "numeric",
month: "long",
})}
</CalendarScheduleItemTitle>
{events.some((event) => event.date === date) ? (
<ol className="grid gap-4">
{events
.filter((event) => event.date === date)
.map((e) => (
<CalendarScheduleItem key={e.id}>
{e.content}
</CalendarScheduleItem>
))}
</ol>
) : (
<p className="text-sm text-muted-foreground">
Nothing scheduled for this day.
</p>
)}
</>
) : (
children
)}
</CalendarScheduleContent>
);
}
registry/ui/use-controllable.ts
Scroll horizontally for long lines
"use client";
import { useRef, useState, type SetStateAction } from "react";
/** Controlled or instance-local state. Functional updates also work before a rerender. */
export function useControllable<T>(
value: T | undefined,
initial: T,
onChange?: (value: T) => void,
) {
const [local, setLocal] = useState(initial);
const current = useRef(value === undefined ? local : value);
current.current = value === undefined ? local : value;
return [
current.current,
(action: SetStateAction<T>) => {
const next =
typeof action === "function"
? (action as (previous: T) => T)(current.current)
: action;
current.current = next;
if (value === undefined) setLocal(next);
onChange?.(next);
},
] as const;
}
registry/ui/utils.ts
Scroll horizontally for long lines
import { clsx, type ClassValue } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...values: ClassValue[]) {
return twMerge(clsx(values));
}
registry/ui/calendar.tsx
Scroll horizontally for long lines
"use client";
import * as React from "react";
import { cn } from "./utils";
import { Button } from "./button";
import { useControllable } from "./use-controllable";
export function dateKey(d: Date) {
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
}
export function Calendar({
children,
value,
defaultValue = "",
onValueChange,
className,
min,
max,
...rootProps
}: Omit<
React.ComponentProps<"div">,
keyof {
value?: string;
defaultValue?: string;
onValueChange?: (value: string) => void;
className?: string;
min?: string;
max?: string;
}
> & {
value?: string;
defaultValue?: string;
onValueChange?: (value: string) => void;
className?: string;
min?: string;
max?: string;
}) {
const [selected, setSelected] = useControllable(
value,
defaultValue,
onValueChange,
);
const [month, setMonth] = React.useState(() =>
selected ? new Date(selected + "T12:00:00") : new Date(),
);
const y = month.getFullYear(),
m = month.getMonth(),
days = new Date(y, m + 1, 0).getDate(),
offset = (new Date(y, m, 1).getDay() + 6) % 7;
const refs = React.useRef<(HTMLButtonElement | null)[]>([]);
return (
<div
{...rootProps}
className={cn(
"w-72 max-w-full rounded-xl border border-border p-4",
className,
)}
>
{children !== undefined ? (
children
) : (
<>
<div className="mb-4 flex items-center justify-between">
<Button
variant="ghost"
aria-label="Previous month"
onClick={() => setMonth(new Date(y, m - 1, 1))}
>
←
</Button>
<span aria-live="polite" className="text-sm font-medium">
{month.toLocaleDateString("en-GB", {
month: "long",
year: "numeric",
})}
</span>
<Button
variant="ghost"
aria-label="Next month"
onClick={() => setMonth(new Date(y, m + 1, 1))}
>
→
</Button>
</div>
<div className="grid grid-cols-7 gap-1">
{["M", "T", "W", "T", "F", "S", "S"].map((d, i) => (
<span
key={i}
aria-hidden="true"
className="py-1 text-center text-xs text-muted-foreground"
>
{d}
</span>
))}
{Array.from({ length: offset }, (_, i) => (
<span key={"s" + i} />
))}
{Array.from({ length: days }, (_, i) => {
const key = dateKey(new Date(y, m, i + 1));
return (
<button
type="button"
key={key}
ref={(el) => {
refs.current[i] = el;
}}
aria-label={new Date(y, m, i + 1).toLocaleDateString(
"en-GB",
{
dateStyle: "full",
},
)}
aria-pressed={selected === key}
disabled={!!((min && key < min) || (max && key > max))}
onClick={() => setSelected(key)}
onKeyDown={(e) => {
const delta = (
{
ArrowLeft: -1,
ArrowRight: 1,
ArrowUp: -7,
ArrowDown: 7,
} as Record<string, number>
)[e.key];
if (delta) {
e.preventDefault();
refs.current[
Math.min(days - 1, Math.max(0, i + delta))
]?.focus();
}
}}
className={cn(
"size-8 max-w-full rounded-lg text-sm hover:bg-muted disabled:opacity-30",
selected === key && "bg-primary text-primary-foreground",
)}
>
{i + 1}
</button>
);
})}
</div>
</>
)}
</div>
);
}
registry/ui/button.tsx
Scroll horizontally for long lines
"use client";
import * as React from "react";
import { Slot } from "radix-ui";
import { LoaderCircle } from "lucide-react";
import { cn } from "./utils";
export type ButtonProps = React.ComponentProps<"button"> & {
variant?: "primary" | "secondary" | "outline" | "ghost" | "danger";
size?: "sm" | "md" | "lg";
loading?: boolean;
asChild?: boolean;
};
export function Button({
variant = "primary",
size = "md",
loading,
asChild,
className,
children,
disabled,
...props
}: ButtonProps) {
const Comp = asChild ? Slot.Root : "button";
return (
<Comp
type={asChild ? undefined : "button"}
disabled={asChild ? undefined : disabled || loading}
aria-disabled={disabled || loading || undefined}
aria-busy={loading || undefined}
className={cn(
"relative inline-flex shrink-0 items-center justify-center gap-2 whitespace-nowrap rounded-lg border border-transparent text-sm font-medium leading-none transition-[background-color,border-color,box-shadow,transform] duration-150 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary active:scale-[.98] disabled:pointer-events-none disabled:opacity-45 aria-disabled:pointer-events-none [&_svg]:shrink-0",
{
"bg-primary text-primary-foreground shadow-[inset_0_1px_0_#ffffff26,0_1px_2px_#00000012] hover:bg-primary/90":
variant === "primary",
"border-border/60 bg-muted text-foreground hover:bg-muted/70":
variant === "secondary",
"border-border bg-background text-foreground shadow-sm hover:bg-muted/60":
variant === "outline",
"text-muted-foreground hover:bg-muted hover:text-foreground":
variant === "ghost",
"bg-danger text-danger-foreground hover:bg-danger/90":
variant === "danger",
},
{
"h-8 px-3 text-xs": size === "sm",
"h-10 px-4": size === "md",
"h-12 px-6": size === "lg",
},
className,
)}
{...props}
>
{asChild ? (
children
) : (
<>
<span
className={cn(
"inline-flex min-w-0 flex-1 items-center justify-center gap-2",
loading && "invisible",
)}
>
{children}
</span>
{loading && (
<LoaderCircle
aria-hidden="true"
className="absolute size-4 animate-spin motion-reduce:animate-none"
/>
)}
</>
)}
</Comp>
);
}
Dependencies
clsx@2.1.1 · tailwind-merge@3.5.0 · radix-ui@1.6.7 · lucide-react@0.577.0
Accessibility & behaviour
Provide meaningful labels and preserve keyboard focus styles. Check contrast when customising theme colours.
Read the accessibility and performance guide →