Calendar
A month drawn as time, after the "28 December" poster: a huge numeral, then a large disc for every day, ink for the days gone, the accent for today, rings for the days to come, and half-moons for the wait. The ruler draws the same month as one measured line; the ghost sets the days against the date huge and faint behind them; the parenthesis runs the month as a line of figures with the wait in parentheses.
Report an issue with CalendarExample
Pick a day for a first call.
No day chosen yet.
September 2026
8 days to go
September 2026
Today is the day
October 2026
31 days to come
October 2026
31 days to come
September 2026
8 days to go
October 2026
23 days to go
October 2026
31 days to come
September 2026
8 days to go
September 2026
14 days left in September
October 2026
31 days to come
September 2026
8 days to go
October 2026
23 days to go
October 2026
31 days to come
Install
Run it in a project that has the shadcn CLI set up. It brings the 0nlyType base along.
npx shadcn@latest add https:// thedirectors.agency/ ui/ r/calendar.jsonUsage
"use client"import * as React from "react"import { Calendar } from "@/components/ui/calendar"const longDay = new Intl.DateTimeFormat("en-GB", { weekday: "long", day: "numeric", month: "long" })// The four share the chosen day, so you can compare how each draws it.export default function Example() {const [day, setDay] = React.useState<Date>()return (<div className="grid w-full gap-8"><p id="calendar-lead">Pick a day for a first call.</p><div className="grid gap-4"><span className="ot-label">dots</span><Calendar disablePast value={day} onValueChange={setDay} aria-labelledby="calendar-lead" /></div><div className="mt-8 grid gap-4"><span className="ot-label">ruler</span><Calendar variant="ruler" disablePast value={day} onValueChange={setDay} aria-labelledby="calendar-lead" /></div><div className="mt-8 grid gap-4"><span className="ot-label">ghost</span><Calendar variant="ghost" disablePast value={day} onValueChange={setDay} aria-labelledby="calendar-lead" /></div><div className="mt-8 grid gap-4"><span className="ot-label">parenthesis</span><Calendar variant="parenthesis" disablePast value={day} onValueChange={setDay} aria-labelledby="calendar-lead" /></div><p aria-live="polite">{day ? (<>Your first call: <span className="ot-yours">{longDay.format(day)}</span>.</>) : ("No day chosen yet.")}</p></div>)}// A fixed today (16 September) and a chosen day (24 September), so every state shows at once:// the days gone, today, the wait, yours and the days to come.const today = new Date(2026, 8, 16)const chosen = new Date(2026, 8, 24)const october = new Date(2026, 9, 1)const dots = "w-[22rem] max-w-full"const ruler = "w-[36rem] max-w-full"Contract
- Underneath: hook (a grid with a roving tab stop).
- From the "28 December" poster: a huge numeral, the month over the year beside it and the weekday at the far side, then a grid of large discs, one per day. The grid of discs is the picture: time spent and time left, at a glance.
- Anatomy:
.ot-month[data-variant="dots | ruler | ghost | parenthesis"]holding.ot-month-head(.ot-month-atwith the numeral.ot-month-dateand.ot-month-dayname, the.ot-month-titlemonth in caps over the year,.ot-month-navwith two hairline chevrons), then.ot-month-gridwith 7<abbr>weekday heads and weeks of<button role="gridcell" class="ot-month-day" data-when="past | today | future" aria-selected>each holding.ot-month-disc>.ot-month-n, today witharia-current="date", then.ot-month-foot. Before the 1st there is only paper (.ot-month-gap); after the last day, nothing: the neighbouring months never show. - dots (default): gone is a solid ink disc, today the one accent disc, to come a hairline ring (
--ot-rule-strong). The number is reversed out of the ink it sits on, like "the uncreative": paper on ink (over 15:1 in day and nocturne), paper on the accent (the accent clears 4.5:1 on paper in every scheme and key, so paper on it does too), and pencil inside a ring so the days to come stay back.--fis how full of ink a disc is, from the bottom. - dots, the wait: the days between today and the chosen day are half-moons (
data-between), after the eclipse poster: ink under a horizon, the number in the open sky above it, no ring. As in the poster, the disc is narrower than its horizon: the moon is about half its day's width, and the horizon is one--ot-rule-stronghairline that runs on through the gaps from today to yours, so the wait reads as one span and stays lighter than the days gone. Once you choose (or turn the month), they wax in turn toward your day (--k×--ot-arpeggio). - dots, yours: the chosen day is circled, like a date on a paper calendar: an ink ring at stroke width outside its disc, its number in the italic, larger. It adds no second accent; chosen today, the accent disc is circled. Pointing sketches the same ring in pencil, drawn round from the top (
--sweep, a conic mask); choosing inks it and it lands with spiccato. - ruler: the month as one measured line, after Paul Rand's dimension lines and SPECTRA's huge type against tiny data. A tick a day (
.ot-month-discrestyled) on a baseline that is ink as far as today; the weeks' first days and the 1st stand taller and carry their number (every day is numbered once the line is 40rem long; a label gives way to a neighbouring today or yours). Today is the accent tick with a dot at its head, yours a full ink tick with a ring at its head and its number in italic. The numeral stands over its tick, clamped inside the line, with the weekday beside it on the side facing the middle. Under the line,.ot-month-spanmeasures the wait as a dimension from today to your day with the count under its middle, open at an end that runs off the month. The rows becomedisplay: contents, so the grid is one row of--dayscolumns; the weekday heads stay for screen readers only. - ghost: after "the silence that heals", numbers as data against huge ghost numerals. The days are figures only, no discs: gone in pencil, to come in ink, today the accent figure (700), yours in the italic. Behind them
.ot-month-atbecomes the ghost: the chosen day (else today) stands over its month's number (.ot-month-mo, "24" over "09"), light (200), at 58cqi of the month's own width so it holds at popover size, in half the rule's tone, cropped by the month's end and foot edges (overflow: clip,container-type: inline-size). The grid is read through it. It turns italic with yours, at four fifths of the size. The head is the month over the year with the chevrons at the far side; the weekday name is left to the column heads. Pointing sketches a pencil hairline under the figure, drawn out from its middle. Forced colours drop the ghost; the italic still marks yours. - parenthesis: after "20(25)", the typographic parenthetical. The month is set as one running line of figures at
mpthat wraps like a sentence (the rows becomedisplay: contents; the weekday heads stay for screen readers only), a wider space (0.6em) closing each week. The time from today to your day, whichever comes first, is an parenthesis:data-paren="open"hangs a "(" before the first of the two anddata-paren="close"a ")" after the other, both light (200) and a little taller than the figures, hung in the space between figures (absolutely placed, like hanging punctuation) so nothing reflows. The figures inside are set heavier (650). An aside that began last month only closes here. Gone is pencil, to come is ink, today the accent, yours the italic. Pointing sketches pencil parentheses round a figure; choosing inks the aside and its parentheses land with spiccato. Right to left, the parentheses mirror with the text (bidi mirroring). - Focus in ghost and parenthesis is a ring, not a box: the day is rounded (
border-radius: 50%) so the outline draws an ellipse round the figure; parenthesis draws it with no offset. - ruler, the hand: the ticks are narrow, so pressing and dragging along the line sketches the day under the pointer in pencil, and letting go chooses it (
data-scrub, pointer capture,touch-action: pan-y). A key's click still chooses. - Yours, in every variant: the big numeral turns to the chosen day in the italic, and the weekday follows it. dots and parenthesis roll it; ruler glides it along the line; ghost sets a new ghost that surfaces.
- The foot: one tracked line in caps that says something true: "12 days to go", "Tomorrow", "Today is the day", "The last day of September", "9 days left in September". English only;
captionreplaces it or, as null, removes it. - Turning the month, the days sweep in, in reading order, from the side you turned toward (
data-turn="next | prev"on the grid,--d= the cell's place), mirrored right to left. - States: rest, gone / today / the wait / yours / to come, chosen today, hover, focus (on the day that holds the tab stop), disabled days, and for ruler the wait run on from another month; ghost with a chosen day, with none (today behind) and pointed; parenthesis enclosing the wait, run on from another month, and pointed. The keyboard is the same in every variant, the running line included.
- Keyboard: roving tabindex; arrow keys move by a day (following the direction) or a week, Page Up and Page Down by month, Home and End to the ends of the week; Enter or Space chooses. Each day is named in full: "Wednesday 30 September 2026, today".
Props
| Prop | Type | Default | What it does |
|---|---|---|---|
value / defaultValue / onValueChange | Date, (date) => void | The chosen day, controlled or not. | |
month / defaultMonth / onMonthChange | Date, (month) => void | Any date in the month on show. Left out, it's the chosen day's month, else today's. | |
today | Date | Which day counts as today. Left out, it's the device's, read once the page has loaded so the server and the browser agree. | |
disablePast | boolean | false | Days before today can't be chosen. |
variant | "dots" | "ruler" | "ghost" | "parenthesis" | "dots" | How time is drawn. dots, the poster: a disc a day, the number reversed out of the ink; the wait as half-moons; yours circled, in italic. ruler: the month as one line with a tick a day, inked as far as today; the numeral stands over the chosen tick and the wait is measured under the line as a dimension. Drag along the ruler to choose. ghost: the days as figures only, read through the chosen day (else today) set over its month's number, huge and faint behind them. parenthesis: the month as one running line of figures, a wider space closing each week, with the time from today to your day enclosed in parentheses. |
caption | ReactNode | null | The tracked line at the foot. Left out, it counts the days in English (12 days to go, The last day of September); pass your own words for another language, or null for none. | |
locale | string | "en-GB" | Month and weekday names. Weeks start on Monday. |
Keyboard | note | One tab stop. Arrow keys move by day and week, Page Up and Page Down by month, Home and End to the ends of the week; Enter or Space chooses. Each day is read in full, such as Wednesday 30 September 2026, today. |
Craft
Where its move comes from
| Component | Reference | Move |
|---|---|---|
| Month | "28 December" dots; half-circle eclipse; "the uncreative" | A disc a day, the number reversed out of the ink; the wait as half-moons; yours circled |
| Month, ruler | Paul Rand dimension lines; SPECTRA | The month as a measured line; the wait as a dimension |
| Month, ghost | "the silence that heals" ghost numerals (14 / 08) | The days as figures, read through the date set huge and faint behind them |
| Month, parenthesis | "20(25)"; the typographic parenthetical | The month as a running line of figures; the wait as an aside in parentheses |
How it moves
| Component | Articulation | What moves |
|---|---|---|
| Month | Arpeggio | A pencil ring draws round the day you point at and inks when you choose, landing with spiccato; the moons wax in turn; the days sweep in from the turn |
| Month, ruler | Legato | The numeral glides along the line to your day and the dimension stretches after it; dragging sketches the day under the hand in pencil |
| Month, ghost | Surfacing | A pencil hairline draws out under the figure you point at; choosing (or turning the month) sets a new ghost that rises into view, the month's number an arpeggio behind the day's |
| Month, parenthesis | Spiccato | Pointing sketches pencil parentheses round a figure; choosing inks the aside and its parentheses land with one small rebound |