@corvu/calendar
Unstyled, accessible calendar primitive for SolidJS with single, multiple, and range date selection.
Repository Health
Technical Analysis
@corvu/calendar is an unstyled, accessible calendar primitive for SolidJS, part of the corvu collection of headless UI building blocks. It renders the date-grid logic — month navigation, weekday headers, day cells, and keyboard-driven focus — without imposing any markup or styling, so teams can build a fully custom calendar or date-picker UI on top of accessible ARIA grid semantics.
The component ships a compound API (Calendar.Root, Label, Nav, Table, HeadCell, Cell, CellTrigger) built around SolidJS’s fine-grained reactivity, and supports single, multiple, and range selection modes out of the box, along with disabled-day predicates, multi-month layouts, fixed six-week grids, and RTL text direction.
What You Get
- A compound component API (Root, Label, Nav, Table, HeadCell, Cell, CellTrigger) that mirrors the semantic structure of a calendar grid
- Built-in single, multiple, and range selection modes with controllable or uncontrolled value and month state
- Keyboard navigation and focus management across cells, including automatic month/year advancement when focus moves outside the visible range
- Full ARIA grid semantics (role=grid/gridcell, aria-selected, aria-disabled) plus data-* attributes for CSS-only styling hooks
- Multi-month rendering, fixed six-week grids, disableOutsideDays, and a disabled-day predicate for custom availability rules
Common Use Cases
- Building a fully custom-styled date picker or booking calendar without fighting a pre-styled component library
- Adding date-range selection to a reservation or scheduling flow with excludeDisabled range logic
- Embedding a multi-month calendar view in a dashboard or admin panel
- Implementing accessible keyboard-only date entry for a form
Under The Hood
Architecture
The package follows a compound-component pattern built entirely on SolidJS’s fine-grained reactivity. CalendarRoot (src/Root.tsx) is the single stateful owner: it derives value/month/focusedDay via the shared @corvu/utils createControllableSignal helper (a dual controlled/uncontrolled switch keyed on whether the corresponding prop is defined), computes weekdays/months/weeks from the current month, and exposes navigation, day-selection (branching per mode: single/multiple/range), and selection/disabled predicates. State is published through two nested Solid contexts — a public CalendarContext consumed via the exported useContext, and an InternalCalendarContext carrying label-id registration and focus-tracking internals — created per-instance via a keyed-context helper so nested calendars don’t collide. Child components (Label, Nav, Table, HeadCell, Cell, CellTrigger) are dumb context consumers rendered through corvu’s shared polymorphic Dynamic/ElementOf machinery, so the rendered tag can change without touching Root. The single biggest coupling point is the mode discriminated union: several @ts-expect-error casts in Root.tsx exist specifically to bridge the single/multiple/range union with concrete signal types, so extending that union would ripple through Root and every typed consumer.
Tech Stack Strict TypeScript targeting ESNext on a SolidJS ^1.8 peer dependency, built with tsup into dual ESM/solid-JSX export conditions, linted with ESLint 10 and typescript-eslint plus eslint-plugin-solid. The only runtime dependency is the workspace-internal @corvu/utils package shared across all corvu primitives (dynamic-element, reactivity, controllable-signal, and keyed-context helpers) — zero external runtime dependencies. It lives in a pnpm/Turborepo monorepo alongside sibling primitives (accordion, dialog, drawer, popover, tooltip, resizable, otp-field, disclosure) and a separate docs site, with releases automated via release-please.
Code Quality
No test files exist anywhere in the package or its shared workspace dependencies — quality is enforced through TypeScript strictness and linting rather than executable tests. tsconfig.json enables strict, strictNullChecks, and noUncheckedIndexedAccess; the ESLint config layers strict-boolean-expressions and no-unnecessary-condition on top of the typescript-eslint recommended set, a stricter-than-typical bar. Naming is consistent and namespaced per component, and JSDoc annotates nearly every public prop with its default value. A CI workflow runs lint on every push. The handful of @ts-expect-error union casts are a deliberate, documented trade-off rather than sloppiness, but the complete absence of automated tests is a real gap for a stateful, interaction-heavy primitive.
API Design
The public API mirrors Radix/Kobalte-style compound components but is built natively on SolidJS primitives rather than ported: value, month, and focusedDay are each independently controllable-or-uncontrolled through the same pattern, so a consumer can leave everything uncontrolled for a minimal calendar or take over just one slice of state without extra wiring. The mode prop is a true discriminated union that changes both the value type and the available props (min/max for multiple, excludeDisabled for range) at the type level — a stronger guarantee than string-enum props with loosely-typed values. Root’s children can also be a render-prop function receiving the same props as the context, giving a full escape hatch for custom layouts. Every emitted element carries purpose-built data-* attributes (data-selected, data-today, data-range-start/end, data-in-range) for CSS-only styling. Nothing here is architecturally novel relative to sibling headless-UI libraries (Kobalte, Ark UI, Radix) — it’s a well-executed application of an established pattern to an ecosystem (SolidJS) with fewer competing options than React.