Styling and theming

Documentation index

Month-view week rows fill the available calendar height by default. Set height (for example, height="800px" or height="100%" inside a parent with a defined height) to size the calendar. Without a defined height, rows size naturally to their content. Crowded weeks can grow to show all events; use dayMaxEvents: true for uniform rows with overflow behind a “+N more” link.

The stylesheet ships as z-cal/style.css (also bundled as z-cal.global.css next to the IIFE build). Import it explicitly when consuming the published package. The polyfill import can be omitted when every target runtime has native Temporal. The CDN bundle includes the polyfill and exposes plugins directly, for example ZCal.DayGrid.

Every rule lives in a cascade layer named z-cal, so any unlayered rule you write wins over the library's regardless of selector specificity:

.cx-event {
  font-weight: 400; /* beats the library's own `.cx-event` rule */
}

Tailwind v4

Declare the complete layer order before either stylesheet is loaded. Put the calendar after Tailwind's base reset and before your component/utility overrides:

@layer theme, base, z-cal, components, utilities;
@import 'tailwindcss';
@import 'z-cal/style.css';

Use this in your global CSS entry, then import that entry from the application. Keep the same ordering in separate editor, preview, or embedded-document entries. Avoid separately importing the calendar CSS before this declaration: the first declaration establishes layer precedence.

Tailwind Preflight lives in base and resets borders. If z-cal is established before base, reset rules can erase calendar borders and CSS-drawn navigation arrows. The order above lets calendar styles restore them while retaining utility overrides. Unlayered application CSS still takes precedence over normal layered rules.

Tokens

The design is driven by a handful of custom properties on .cx. Override them on .cx (or any selector that matches the calendar root):

.cx {
  --cx-accent: oklch(60% 0.18 150);
  --cx-surface: #fbfbfb;
  --cx-text: #1a1a1a;
  --cx-radius: 12px;
  --cx-font-size: 0.8125rem;
}
TokenDefaultUsed for
--cx-accentblue, light-dark(oklch(55% 0.2 265), oklch(74% 0.14 265))default event tint, focus ring, today column heading, highlighted dates
--cx-surfacelight-dark(#fff, #0c0e12)cell, header and popup background
--cx-textlight-dark(#181d27, #ececed)text; muted text, borders and grid lines are mixed from it
--cx-radius8pxouter frame, buttons, popup (--cx-radius-sm is derived for event pills)
--cx-font-size0.875remroot font size; everything else scales in em / rem
--cx-font-familyinheritroot font
--cx-shadow-colorgrey / blackthe colour behind --cx-shadow and --cx-shadow-lg

These are derived from the primary tokens with color-mix() and can be overridden individually:

TokenDerivationUsed for
--cx-surface-mutedtext 3% into surfacecolumn headings, list day headings, other-month cells, button hover
--cx-bordertext 10% into surfaceouter frame, button borders
--cx-grid-linevar(--cx-border)cell borders, hour lines
--cx-grid-line-minorborder 55% into surfaceslot (half-hour) lines
--cx-text-mutedtext 62% into surfaceheadings, times, "+N more"
--cx-focus-ringvar(--cx-accent):focus-visible outline
--cx-today-badge-bg / --cx-today-badge-texttext / surfacethe round badge on today's day number
--cx-highlight-coloraccent 8% into surfacehighlightedDates cells
--cx-weekend-bgvar(--cx-surface)Saturday / Sunday cells; set to var(--cx-surface-muted) to tint weekends
--cx-non-business-bgtext 4% into transparentany background over time outside business hours
--cx-selection-bgaccent 16%the range-selection preview
--cx-button-bg, --cx-button-hover-bg, --cx-button-active-bg, --cx-button-bordersurface-basedtoolbar buttons
--cx-now-indicator-colorredthe current-time line

Time-grid and resource time-grid views can fill a constrained calendar height with expandRows: true. slotHeight stays the minimum; use the option so the rendered rows and time geometry remain aligned.

Layout properties written by the views (--cx-slot-height, --cx-slot-width, --cx-sidebar-width, --cx-grid-cols, …) are internal; drive them through options such as slotHeight and slotWidth.

Dark mode

Colours are defined with light-dark(), so the calendar follows the color-scheme of the page. Set color-scheme: dark (or light dark) on html and the calendar switches with it.

To pin the calendar independently of the page, use the colorScheme option ('light', 'dark' or the default 'auto'):

import 'z-cal/polyfill'
import { Calendar, DayGrid } from 'z-cal/react'
import 'z-cal/style.css'

const plugins = [DayGrid]
export function ThemedCalendar({ dark }: { dark: boolean }) {
  return <Calendar plugins={plugins} view="dayGridMonth" colorScheme={dark ? 'dark' : 'light'} />
}

The option adds cx-light / cx-dark to the root; the same classes work on any ancestor (<body class="cx-dark">) because they only set color-scheme.

Business hours

Time outside businessHours is painted by an overlay inside each day cell, above its background and grid lines and below its content. Time grids and timelines mask the overlay to the closed slots; day grids, the all-day row, and month timelines show it over whole closed days, which also carry the nonBusiness class (cx-non-business). The overlay's background is --cx-non-business-bg, so it takes anything a background does: a colour, a color-mix() of your own tokens, or a gradient pattern. It mixes from --cx-text by default, so it follows dark mode on its own.

/* A stronger flat tint. */
.cx {
  --cx-non-business-bg: color-mix(in oklab, var(--cx-text) 8%, transparent);
}

/* Diagonal hatching instead of a tint. */
.cx.hatched {
  --cx-non-business-bg: repeating-linear-gradient(
    -45deg,
    transparent 0 5px,
    color-mix(in oklab, var(--cx-text) 10%, transparent) 5px 6px
  );
}

Set the token on .cx, any ancestor, or a class of your own on the root; with Tailwind, a rule in components (or unlayered CSS) wins over the z-cal layer. To style whole closed days differently from closed slots, target the class:

.cx-day.cx-non-business {
  --cx-non-business-bg: var(--cx-surface-muted);
}

Remap the class through theme: { nonBusiness: 'closed' } when your design system owns the name. The mask and display of the overlay (--cx-non-business-mask, --cx-non-business-display) are written by the views and are internal, like the other layout properties.

Event colours

  • color is an accent: the stylesheet derives a soft tint (pale background, coloured text and border in light mode; a deeper tint with light text in dark mode) and the small dot in front of the title. Consumer colours therefore adapt to both schemes.
  • backgroundColor and textColor are used verbatim (the event gets cx-event-solid).
  • The calendar-wide defaults are eventColor, eventBackgroundColor and eventTextColor; resources may set eventBackgroundColor / eventTextColor.

The resolved values reach the element as --cx-event-accent, --cx-event-bg and --cx-event-text, so a stylesheet can restyle them too. For example, to render every event as a solid fill:

.cx {
  --cx-event-bg: var(--cx-accent);
  --cx-event-text: #fff;
}

Hide the accent dot with --cx-event-dot: none (the demo uses this for plain event text).

Class names

Every class the renderer emits is listed in the theme option and can be remapped or extended:

import type { CalendarOptions } from 'z-cal'

export const options: CalendarOptions = {
  theme: { event: 'cx-event my-event', dark: 'dark' },
}

React passes the same option through its theme prop. Retain the original class when you want to extend rather than replace built-in styling. nonBusiness marks day cells whose whole date is outside business hours. keyboardCursor styles the focused-slot marker of keyboard navigation and liveRegion its visually hidden announcer; the root carries data-cx-keyboard while a day cell has focus, and --cx-focus-ring colours both the cell outline and the cursor.

Event attributes

eventAttrs adds attributes to every event element, statically or per event, so application state such as a selected event reaches the DOM without imperative element access:

import type { CalendarOptions } from 'z-cal'

export function optionsFor(selectedId: string | null): CalendarOptions {
  return {
    eventAttrs: (info) => ({
      'aria-selected': info.event.id === selectedId,
      'data-calendar': info.event.extendedProps['calendar'] as string | undefined,
    }),
  }
}

true sets an empty attribute, false, null, and undefined remove it, and attributes the function stops returning are removed on the next render. The element's own role, tabindex, and data-cx-event cannot be overridden. Pass a new function (or object) when the state it reads changes; React props do this naturally.

Custom content and icons

Use React content slots for JSX. In vanilla code, eventContent accepts text or { domNodes: Node[] }. Explicit { html: string } content is trusted HTML; sanitize untrusted markup before supplying it. Plain strings are rendered as text. Adding a renderer replaces the event body, so include its time and title when you want to retain them.