Calendar
Date-grid calendar (react-day-picker) re-skinned to the HUD — sharp cells, mono uppercase weekday rulers, ghost nav and a signal fill on the selected day.
"use client";import * as React from "react";import { Calendar } from "@/registry/okibi/ui/calendar";export default function CalendarDemo() { const [date, setDate] = React.useState<Date | undefined>(new Date()); return ( <Calendar mode="single" selected={date} onSelect={setDate} className="border border-border-strong" /> );}npx shadcn@latest add https://okibi.cndr.dev/r/calendar.jsonInstallation
Install the theme first, then add the component:
npx shadcn@latest add https://okibi.cndr.dev/r/calendar.jsonInstall the required dependencies:
npm install lucide-react react-day-pickerAdd the okibi building blocks it composes: button.
Copy the source from the Code tab above into components/ui/calendar.tsx. It uses the cn helper and the okibi theme tokens — install the theme first if you haven't.
Usage
import { Calendar } from "@/components/ui/calendar"
const [date, setDate] = React.useState<Date | undefined>(new Date())
<Calendar mode="single" selected={date} onSelect={setDate} />Examples
Range
Set mode="range" to select a span: the two ends fill signal with a dim surface strip running between them.
"use client";import * as React from "react";import { type DateRange } from "react-day-picker";import { Calendar } from "@/registry/okibi/ui/calendar";export default function CalendarRangeDemo() { const [range, setRange] = React.useState<DateRange | undefined>(() => { const from = new Date(); const to = new Date(); to.setDate(to.getDate() + 5); return { from, to }; }); return ( <Calendar mode="range" selected={range} onSelect={setRange} className="border border-border-strong" /> );}Multiple
Set mode="multiple" to select any number of individual days; the bound value is a Date[].
"use client";import * as React from "react";import { Calendar } from "@/registry/okibi/ui/calendar";export default function CalendarMultipleDemo() { const [dates, setDates] = React.useState<Date[] | undefined>(() => { const today = new Date(); const next = new Date(); next.setDate(today.getDate() + 2); return [today, next]; }); return ( <Calendar mode="multiple" selected={dates} onSelect={setDates} className="border border-border-strong" /> );}Dropdown navigation
captionLayout="dropdown" swaps the month/year caption for selects, bounded by startMonth/endMonth for fast travel across a wide range.
"use client";import * as React from "react";import { Calendar } from "@/registry/okibi/ui/calendar";export default function CalendarDropdownDemo() { const [date, setDate] = React.useState<Date | undefined>(new Date()); return ( <Calendar mode="single" selected={date} onSelect={setDate} captionLayout="dropdown" startMonth={new Date(2020, 0)} endMonth={new Date(2030, 11)} className="border border-border-strong" /> );}Disabled dates
Pass a disabled matcher to lock out days — here weekends — so only valid dates can be picked.
"use client";import * as React from "react";import { Calendar } from "@/registry/okibi/ui/calendar";export default function CalendarDisabledDemo() { const [date, setDate] = React.useState<Date | undefined>(undefined); return ( <Calendar mode="single" selected={date} onSelect={setDate} disabled={{ dayOfWeek: [0, 6] }} className="border border-border-strong" /> );}API Reference
Calendar
| Prop | Type | Default | Description |
|---|---|---|---|
mode | "single" | "multiple" | "range" | "single" | Selection mode. |
selected | Date | Date[] | DateRange | – | The current selection (matches mode). |
onSelect | (value) => void | – | Fires with the new selection. |
captionLayout | "label" | "dropdown" | "label" | Month/year caption as a label or dropdowns. |
showOutsideDays | boolean | true | Render days spilling in from adjacent months. |
buttonVariant | Button variant | "ghost" | Variant used for the nav buttons. |
numberOfMonths | number | 1 | How many months to render side by side. |
Accessibility
- Built on
react-day-picker— days are a realgridwith arrow-key navigation;autoFocuslands focus on the active day. - Roving focus is honored:
CalendarDayButtonre-focuses whenever itsfocusedmodifier flips, so keyboard movement never strands focus. - The focused cell gets a 2px inset
ring-ring(inset, not offset, so it never clips inside the grid); nav buttons inherit theButtonfocus ring. - Disabled and outside days carry
aria-disabled/ muted styling but stay in the reading order so the month grid keeps its shape. - Chevrons flip under
rtl, and weekday rulers are real<th>headers — screen readers announce the column for each day.
Guidelines
Do
- Pass
modeto match the task —singlefor one date,rangefor spans,multiplefor scattered picks. - Use
startMonth/endMonthto fence navigation and thedisabledmatcher to block invalid days at the source. - Switch
captionLayouttodropdownwhen users jump across distant months or years.
Don't
- Don't lean on the
todaycell as the only state cue — it reads like a range strip once a selection covers it; pair it with a visible label. - Don't drop a bare Calendar into a tight field; for a single value reach for
DatePicker, which wraps it in a popover.
Form
Accessible form scaffolding for react-hook-form — binds the okibi field primitives to a schema, wires `aria-invalid`/`aria-describedby`, and surfaces validation as a mono system line.
Date Picker
An outline-tech trigger that reads the chosen date in a mono readout and opens the Calendar on a hard-shadow popover. Controlled via `value`/`onValueChange`, so it drops straight into a Form field.