Calendar & schedule
A real events calendar: a month Calendar whose `renderDay` marks days with a scheduled event, a Past Event list below it, and an Upcoming Event list on the right — each row date-badged and color-coded — with a real Pagination control underneath.
Built with
How it’s built
- 01
renderDaycompares each cell's date against a list of real event dates (computed from today, not hardcoded) to decide whether to draw the small dot underneath. - 02Pagination's own numbered arrows are hidden (
hideNavigation) so the surroundingPrev/Nexttext links can flank it — the same real component, styled to fit a text-first pagination bar. - 03Every date on this page — the selected day, the event dots, the upcoming list — is computed as an offset from the real current date, so the pattern never looks stuck in a past year.
Source
Two files · copy & pasteStyling: this source uses Tailwind utility classes and this site’s colour tokens (for example text-ink, bg-paper and border-line). The oks-ui components render without them; swap those classes for your own.
import { useMemo, useState } from "react";
import { Button } from "oks-ui/button";
import { Calendar } from "oks-ui/calendar";
import type { CalendarDayContext } from "oks-ui/calendar";
import { Divider } from "oks-ui/divider";
import { Pagination } from "oks-ui/pagination";
import { ChevronRightIcon } from "./icons";
const toISODate = (d: Date) =>
`${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
const isSameDate = (a: Date, b: Date) =>
a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate();
type UpcomingEvent = {
id: number;
dayOffset: number;
time: string;
price: string;
venue: string;
title: string;
description: string;
};
// Built off real day offsets from "today" rather than fixed 2019 dates —
// this component only ever renders client-side (PatternPreview loads every
// pattern with `ssr: false`), so there's no server/client hydration
// mismatch to worry about.
function buildUpcoming(): UpcomingEvent[] {
return [
{ id: 1, dayOffset: 2, time: "12:30pm", price: "Free", venue: "Beacon Coworking, Riverside", title: "Startup metrics and dashboards", description: "The five key metrics that drive an early-stage startup, and how to build a dashboard to track them." },
{ id: 2, dayOffset: 11, time: "6:30pm", price: "Free", venue: "Foundry Hall, Docklands", title: "All things distributed systems", description: "A conversation on distributed systems design, consensus, and the trade-offs nobody tells you about." },
{ id: 3, dayOffset: 19, time: "6:00pm", price: "$24.00", venue: "Foundry Hall, Docklands", title: "Introduction to Web Accessibility", description: "Accessibility looks complex from the outside — this workshop unravels the essentials any team can apply." },
{ id: 4, dayOffset: 26, time: "6:30pm", price: "Free", venue: "Foundry Hall, Docklands", title: "Founder Meetup: Scaling Sales", description: "A working session on scaling a sales team from the first hire through the first ten reps." },
{ id: 5, dayOffset: 47, time: "5:30pm", price: "Free", venue: "Beacon Coworking, Riverside", title: "5 Steps to Growth: Lessons from a Scaleup", description: "An exclusive session for the local startup community on what actually moved the growth needle." },
];
}
const PAST_EVENTS = [
{ id: 1, title: "Introduction to Web Accessibility", description: "Many small teams don't get the reach they want from launch day due to overlooked accessibility basics.", daysAgo: 12 },
{ id: 2, title: "Introduction to Content Marketing", description: "Many small businesses don't get the results they want from content due to inconsistent publishing.", daysAgo: 19 },
];
const EVENT_COLORS = [
"var(--oks-color-info-500)",
"var(--oks-color-primary-600)",
"var(--oks-color-warning-500)",
"var(--oks-color-success-500)",
"var(--oks-color-danger-500)",
];
const CalendarSchedule = () => {
const [today] = useState(() => new Date());
const [selected, setSelected] = useState(() => toISODate(today));
const [month, setMonth] = useState<Date>(() => new Date(today.getFullYear(), today.getMonth(), 1));
const [page, setPage] = useState(2);
const upcoming = useMemo(() => buildUpcoming(), []);
const eventDates = useMemo(
() => upcoming.map((e) => new Date(today.getFullYear(), today.getMonth(), today.getDate() + e.dayOffset)),
[today, upcoming],
);
const monthLabel = month.toLocaleDateString("en-US", { month: "long" });
const yearLabel = month.getFullYear();
const renderDay = ({ date, inMonth }: CalendarDayContext) => {
const hasEvent = inMonth && eventDates.some((d) => isSameDate(d, date));
return (
<span className="relative flex h-full w-full items-center justify-center">
{date.getDate()}
{hasEvent ? (
<span aria-hidden="true" className="absolute bottom-0.5 h-1 w-1 rounded-full" style={{ background: "var(--oks-color-info-500)" }} />
) : null}
</span>
);
};
return (
// @container has to live on its OWN wrapper, not the grid element whose
// @3xl:grid-cols is conditioned by it — an element can't query its own
// container for its own styling (circular per the CSS Containment
// spec), so that utility would silently be evaluated against the
// wrong (ancestor) container and never apply.
<div className="@container w-full">
<div className="grid grid-cols-[minmax(0,1fr)] items-start gap-6 bg-paper p-6 @3xl:grid-cols-2">
{/* calendar + past events */}
<div className="flex flex-col gap-5 @3xl:border-r @3xl:border-line @3xl:pr-6">
<div className="flex items-center justify-between">
<h2 className="text-[1.15rem] font-bold text-ink">
{monthLabel} <span className="font-normal text-ink-muted">{yearLabel}</span>
</h2>
<div className="flex items-center gap-1">
<Button
isIconOnly
aria-label="Previous month"
variant="bordered"
radius="full"
size="xs"
onClick={() => setMonth((m) => new Date(m.getFullYear(), m.getMonth() - 1, 1))}
>
‹
</Button>
<Button
isIconOnly
aria-label="Next month"
variant="bordered"
radius="full"
size="xs"
onClick={() => setMonth((m) => new Date(m.getFullYear(), m.getMonth() + 1, 1))}
>
›
</Button>
</div>
</div>
<Calendar
value={selected}
onChange={(v) => v && setSelected(v)}
month={month}
onMonthChange={setMonth}
weekStartsOn={1}
renderDay={renderDay}
classNames={{ base: "w-full", header: "hidden" }}
/>
<Divider />
<div className="flex flex-col gap-4">
<h3 className="text-[1.05rem] font-bold text-ink">Past Event</h3>
{PAST_EVENTS.map((ev) => {
const date = new Date(today.getFullYear(), today.getMonth(), today.getDate() - ev.daysAgo);
return (
<div key={ev.id} className="flex flex-col gap-1">
<Button variant="link" size="sm" className="h-auto self-start whitespace-normal px-0 text-left font-semibold">
{ev.title}
</Button>
<p className="text-[0.85rem] text-ink-muted">{ev.description}</p>
<p className="text-[0.78rem] text-ink-faint">
{date.toLocaleDateString("en-US", { day: "2-digit", month: "short", year: "numeric" })}
</p>
</div>
);
})}
</div>
</div>
{/* upcoming events */}
<div className="flex flex-col gap-5">
<div className="flex items-center justify-between">
<h2 className="text-[1.15rem] font-bold text-ink">Upcoming Event</h2>
<div className="flex items-center gap-1">
<Button isIconOnly aria-label="Previous events" variant="bordered" radius="full" size="xs">
‹
</Button>
<Button isIconOnly aria-label="More events" variant="bordered" radius="full" size="xs">
›
</Button>
</div>
</div>
<div className="flex flex-col divide-y divide-line">
{upcoming.map((ev, i) => {
const date = new Date(today.getFullYear(), today.getMonth(), today.getDate() + ev.dayOffset);
return (
<div key={ev.id} className="flex gap-3 py-4 first:pt-0">
<div className="w-12 shrink-0 text-center">
<p className="text-[1rem] font-bold text-ink">{date.toLocaleDateString("en-US", { day: "numeric" })}</p>
<p className="text-[0.75rem] uppercase text-ink-faint">{date.toLocaleDateString("en-US", { month: "short" })}</p>
</div>
<span aria-hidden="true" className="w-1 shrink-0 self-stretch rounded-full" style={{ background: EVENT_COLORS[i % EVENT_COLORS.length] }} />
<div className="min-w-0 flex-1">
<p className="text-[0.78rem] font-medium text-[color:var(--oks-color-info-700)]">{ev.venue}</p>
<p className="text-[0.95rem] font-semibold text-ink">{ev.title}</p>
<p className="text-[0.78rem] text-ink-faint">
{ev.time} · <span className="text-[color:var(--oks-color-info-700)]">{ev.price}</span>
</p>
<p className="mt-1 truncate text-[0.82rem] text-ink-muted">{ev.description}</p>
</div>
<Button isIconOnly aria-label={`View ${ev.title}`} variant="bordered" size="sm" className="shrink-0 self-center">
<ChevronRightIcon size={16} />
</Button>
</div>
);
})}
</div>
<div className="flex items-center justify-center gap-2">
<Button variant="link" size="sm" isDisabled={page === 1} onClick={() => setPage((p) => Math.max(1, p - 1))}>
‹ Prev
</Button>
<Pagination page={page} pageCount={5} onChange={setPage} size="sm" hideNavigation />
<Button variant="link" size="sm" isDisabled={page === 5} onClick={() => setPage((p) => Math.min(5, p + 1))}>
Next ›
</Button>
</div>
</div>
</div>
</div>
);
};
export default CalendarSchedule;It imports ./icons, so save this next to it:
export const ChevronRightIcon = ({ size = 18 }: { size?: number }) => {
return (
<svg
width={size}
height={size}
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth={2}
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden="true"
>
<path d="m9 6 6 6-6 6" />
</svg>
);
};More patterns

Analytics dashboardApp screens
A full fintech app screen — collapsible sidebar, header search, wallet cards, an earnings chart, savings goals, and a transactions table.

Settings panelApp screens
A real settings screen — top tabs, two-column sections, and a sortable, selectable billing history table.

Data table viewApp screens
A full documents list screen — status Tabs, a filter toolbar, and a sortable, selectable Table with real recipient avatars.