calendar-tsx

Themes

Plain CSS for the parts, with a positioning base, built-in themes and a density.

@calendar-tsx/themes styles the parts of the React and Solid bindings. The stylesheets select parts by their data-scope/data-part attributes and position them from the --cal-* geometry the parts emit. They need no JavaScript and no class names.

Install

npm i @calendar-tsx/themes

Import one theme, then optionally a density and the print layer:

import '@calendar-tsx/themes/default.css';
import '@calendar-tsx/themes/compact.css';
import '@calendar-tsx/themes/print.css';

Files

FileContents
base.cssPositioning only: day-grid bars, time-grid columns and lanes, timeline rows, the now line. Themes import it.
default.cssbase.css plus the default look.
high-contrast.cssBlack on white and white on black, with outlined events.
shadcn.cssThe default look with its tokens read from shadcn/ui's variables.
material-ui.cssThe default look with its tokens read from Material UI's CSS theme variables.
revenuecat.cssThe default look in RevenueCat's brand colors.
stripe.cssThe default look in Stripe's colors.
fullcalendar.cssFullCalendar's classic theme, for apps migrating from it.
google-calendar.cssGoogle Calendar's web look.
compact.cssDensity only: smaller slots, bars and text. Import it after any theme.
print.cssPrint only: paper colors, outlined events, no scrolling. Import it after any theme.

Import base.css alone to write your own look on a working layout.

Layers

Every file declares the same cascade layers, in this order:

@layer calendar.base, calendar.theme, calendar.density, calendar.print;

base.css fills calendar.base, themes fill calendar.theme, compact.css fills calendar.density and print.css fills calendar.print. A density therefore wins over any theme, and print over both, whatever the import order. Every selector is wrapped in :where(), with zero specificity.

Your unlayered CSS always wins over the themes without !important. CSS in your own layers wins only when those layers come after the calendar's. See overriding rules.

Expected composition

base.css expects the parts nested as the view pages show them:

  • DayGridView holds sections. Each DayGridViewSection holds its header and rows, and each DayGridViewRow holds its DayGridViewDay cells and DayGridViewRowEvent bars.
  • TimeGridView holds TimeGridViewHeader, an optional TimeGridViewAllDay and TimeGridViewBody. The body holds an optional TimeGridViewAxis and the TimeGridViewColumns.

Built-in themes

Each theme imports base.css itself, so import exactly one.

Loading the preview…

Live, with sample events. Click an event to see what it reports.

The playground shows every theme in every view, in light and dark, with a token editor and a contrast readout.

Default

import '@calendar-tsx/themes/default.css';

A neutral gray look with an ink accent for today, blue events and a red now line. Colors follow the inherited color-scheme through light-dark(), and every value is a token. It sets no font family, so the calendar inherits the page's. Events, today, now, background events, unavailable time and weekends each get their own color; see roles.

High contrast

import '@calendar-tsx/themes/high-contrast.css';

high-contrast.css replaces default.css for people who choose more contrast: black on white in light mode, white on black in dark mode.

  • Every text pairing is 9:1 or more, above WCAG AAA's 7:1.
  • Borders are 5.3:1 or more against the surface. Against muted surfaces, such as header bands, they are 4.47:1 in light mode and 5.1:1 in dark mode.
  • Timed events are outlined instead of tinted: the surface with a 2px border, a 4px stripe in the text color and semibold text. All-day and multi-day bars are solid in the text color. --calendar-event and --calendar-event-border still override them.
  • A bar that continues past a week edge, or a time-grid event that continues past midnight, gets a dashed edge, so continuation does not depend on a missing corner radius.
  • Focus rings are 3px, and corners are tighter.
  • The accent is deep blue (#0035a8) in light mode and yellow (#ffd500) in dark mode. The now line is red (#c00000, 6.48:1; #ff8080, 8.65:1) and background events are green (#006b4f, #5fe0b5).

This theme is separate from Windows High Contrast. Under forced-colors: active the browser chooses the colors, and every theme falls back to the default theme's forced-colors rules.

shadcn/ui

@import 'tailwindcss';
@import '@calendar-tsx/themes/shadcn.css' layer(components);

shadcn.css replaces default.css. It maps the calendar tokens onto shadcn/ui's variables, so the calendar follows the app's palette, radius and dark mode with no further setup:

Calendar tokenshadcn/ui variable
--calendar-surface, --calendar-surface-muted--background, --muted
--calendar-text, --calendar-text-muted--foreground, --muted-foreground
--calendar-border--border
--calendar-accent, --calendar-accent-contrast--primary, --primary-foreground
--calendar-event-color, --calendar-event-color-contrast--chart-1, --foreground
--calendar-now--destructive
--calendar-background-event-color--chart-2
--calendar-focus-ring--ring
--calendar-radius, --calendar-bar-radius--radius, --radius × 0.6
  • Events take --chart-1, apart from today's --primary. shadcn's --secondary, --muted and --accent sit almost on the background in light mode, so they would not separate events from the grid.
  • Solid all-day bars write --foreground on --chart-1. shadcn's default charts are mid-lightness in light mode and mid-darkness in dark mode, so the pair reads in both. With a custom chart palette, check it or set --calendar-event-color-contrast.
  • It expects Tailwind v4 variable naming: full oklch() colors, not the v3 HSL channels.
  • Dark mode follows shadcn's .dark class, on <html> or on any element around the calendar. color-scheme is not needed.

Material UI

import '@calendar-tsx/themes/material-ui.css';

material-ui.css replaces default.css. It maps the calendar tokens onto the CSS theme variables Material UI emits with cssVariables enabled, so the calendar follows the app's palette, shape and color scheme:

Calendar tokenMaterial UI variable
--calendar-surface, --calendar-surface-muted--mui-palette-background-paper, --mui-palette-action-hover
--calendar-text, --calendar-text-muted--mui-palette-text-primary, --mui-palette-text-secondary
--calendar-border--mui-palette-divider
--calendar-accent, --calendar-accent-contrast--mui-palette-primary-main, --mui-palette-primary-contrastText
--calendar-event-color, --calendar-event-color-contrast--mui-palette-secondary-main, --mui-palette-secondary-contrastText
--calendar-now--mui-palette-error-main
--calendar-background-event-color--mui-palette-success-main
--calendar-radius, --calendar-bar-radius--mui-shape-borderRadius

Enable the variables in the MUI theme:

const theme = createTheme({
  cssVariables: { colorSchemeSelector: 'class' },
  colorSchemes: { light: true, dark: true },
});
  • Without cssVariables, Material UI emits no variables and the calendar loses its colors.
  • It expects the default mui variable prefix and :root as the rootSelector.
  • Events take the secondary color, apart from today's primary.
  • The muted surface is MUI's translucent hover color, which always sits on the opaque paper.
  • Dark mode follows MUI's built-in colorSchemeSelector values: media, or class, data and data-mui-color-scheme set on <html> or on any element around the calendar. color-scheme is not needed. A custom selector, such as [data-theme="%s"], works only on <html>.

RevenueCat

import '@calendar-tsx/themes/revenuecat.css';

revenuecat.css replaces default.css with RevenueCat's press-kit colors: monochrome black purple (#1f1f47) with the brand colors as accents.

  • The brand red (#f2545b) marks today and the now line, with black purple numbers on it.
  • Royal blue (#576cdb, with white at 4.59:1) colors events and draws the focus ring. Dark mode uses a lighter blue (#8b9bf0) with black purple text, and black purple as the surface.
  • On white the brand red is 3.39:1, too low for small text, so the theme keeps it to fills and lines. In light mode today's time-grid header uses the text color instead.

Stripe

import '@calendar-tsx/themes/stripe.css';

stripe.css replaces default.css with the colors from the public --hds-color-* tokens on stripe.com.

  • Blurple #533afd (brand-600) colors events and the focus ring. It is a step deeper than the logo's #635bff and takes white text at 6.19:1. Dark mode uses #7389ff.
  • Today is marked in slate (#061b31 in light mode, white in dark mode), apart from the events.
  • The now line is ruby (#e2225f, #ea2261 in dark mode), and background events are green (#00b261).
  • Neutrals are slate: #061b31 text on white in light mode, white on #0d1738 in dark mode.
  • Borders are one step stronger than Stripe's quiet borders, which at 1.2:1 would lose the grid.

This theme is not affiliated with or endorsed by Stripe. Stripe's Marks Usage Terms cover its names, design elements and trade dress.

FullCalendar classic

import '@calendar-tsx/themes/fullcalendar.css';

fullcalendar.css reproduces FullCalendar's classic theme (v7.1.3) on the calendar's parts, so an app moving off FullCalendar keeps its look while it migrates.

  • Today washes its cell and time-grid column in yellow (#facc1526) instead of taking a marker. Day numbers sit at the top end of the cell, underlined on hover.
  • Events are solid #3788d8 with white 12px text and a 4px radius. A timed single-day event in a day grid is an 8px dot and a bold title, with no fill. List rows lead with a 10px dot.
  • Weekends are unshaded and outside-month dates faint (#9ca3af). Non-business time is a flat faint fill, and background events are a 15% green wash with no edge.
  • The time grid has 25px slots with dotted slot lines and solid hour lines, and a 1px red (#ef4444) now line with an arrow at its start. List days are a full-width bold header band above their events.
  • Borders are #ddd, text #030712, corners square.

The tokens read FullCalendar's own --fc-classic-* variables first and fall back to the classic palette, so an app that customized FullCalendar's palette keeps its colors. Set those variables on :root or on the element that carries data-color-scheme, as in FullCalendar. data-color-scheme="dark" switches to the classic dark palette; color-scheme: dark works too.

TokenFullCalendar variable
--calendar-surface, --calendar-surface-muted--fc-classic-background, --fc-classic-muted
--calendar-text, --calendar-text-muted--fc-classic-foreground, --fc-classic-muted-foreground
--calendar-border, --calendar-border-strong--fc-classic-border, --fc-classic-strong-border
--calendar-accent, --calendar-focus-ring--fc-classic-primary
--calendar-event-color, --calendar-event-color-contrast--fc-classic-event, --fc-classic-event-contrast
--calendar-now, --calendar-background-event-color--fc-classic-now, --fc-classic-background-event
today's wash, non-business fill, outside-month dates--fc-classic-today, --fc-classic-faint, --fc-classic-faint-foreground

Differences from FullCalendar:

  • You render event content. FullCalendar renders times such as "9a" and "+2 more"; the calendar renders what you put in each part.
  • FullCalendar gives timed events a 15px minimum height (eventMinHeight). Set minEventMinutes: 18 on the view for the same height at 25px slots. At the default 15 minutes, short events drop their top and bottom borders so their 11px line still fits.
  • List rows keep a 44px minimum height for touch, where FullCalendar's are about 37px.
  • Timeline slot lines are solid rather than dotted.
  • Outside-month dates are 2.54:1 against white, below WCAG AA, as in FullCalendar. Set --fc-classic-faint-foreground to raise them.
  • White event text on #3788d8 is 3.7:1, below WCAG AA, as in FullCalendar. Set --fc-classic-event to a darker blue to raise it.

This theme is not affiliated with or endorsed by FullCalendar.

Google Calendar

import '@calendar-tsx/themes/google-calendar.css';

google-calendar.css gives the calendar Google Calendar's web look. The light values were measured from Google Calendar's rendered styles in October 2026.

  • Today is a filled circle in the primary blue (#0b57d0, white number): 24px in a month, 46px in a week's header, where today's weekday label turns blue too. Day numbers are centered. Weekday labels are 11px spaced capitals in #444746.
  • Events are solid #3f51b5, one of Google Calendar's event colors, with white 12px text and a 6px radius. A timed single-day event in a month is an 8px dot and its text, with no fill. List rows lead with a 12px dot.
  • The time grid draws hour lines only, 40px apart, and a 2px #db372d now line with a 12px dot.
  • Lines are #dde3ea and text #1f1f1f. Weekends and today's column are unshaded. The calendar is a card with 28px corners and no outline; place it on #f8fafd, Google Calendar's app background.

For the closest match, put the weekday outside the day-number trigger in time-grid headers, so the label sits above the number, and load Google Sans if your app has a license for it. The theme sets no font.

Dark mode uses Google's Material 3 dark values for the same roles (#131314 surface, #333537 lines, #e3e3e3 text, #a8c7fa today), read from Google Calendar's stylesheet. Which role each part takes in dark mode is inferred rather than measured. The event and now colors stay the light ones, so the event blue is 2.7:1 against the dark surface, below the 3:1 a dot should have.

Google Calendar has no timeline view; the timeline keeps the default theme's layout in these colors. Google's hour height depends on its density setting; the 40px measured here is --calendar-slot-height: 20px.

This theme is not affiliated with or endorsed by Google. Google Calendar is a trademark of Google LLC.

Compact density

import '@calendar-tsx/themes/default.css';
import '@calendar-tsx/themes/compact.css';

compact.css sets size tokens only, for dashboards, sidebars and wide resource grids. It works with any theme.

TokenDefaultCompact
--calendar-font-size0.875rem0.75rem
--calendar-font-size-sm0.75rem0.6875rem
--calendar-font-size-xs0.6875rem0.625rem
--calendar-day-header-height2.25rem1.75rem
--calendar-bar-height1.375rem1.125rem
--calendar-bar-gap2px1px
--calendar-overflow-height1.5rem1.125rem
--calendar-row-min-height6rem4.5rem
--calendar-slot-height3rem2rem
--calendar-axis-width4rem3.25rem

The density sets these tokens on :root in a later layer. A token you set on an element around the calendar still wins, because that element is closer than :root. The values are absolute: with a look that already uses smaller sizes, such as fullcalendar.css (25px slots) or google-calendar.css (20px slots), compact.css makes slots taller. Set the tokens yourself instead.

Print

import '@calendar-tsx/themes/default.css';
import '@calendar-tsx/themes/print.css';

print.css applies only when printing, on top of any theme and density, so the printed calendar needs no separate view. It fills the calendar.print layer and changes nothing on screen.

  • Browsers leave backgrounds out of a print unless the reader opts in, so nothing relies on a fill. Events, solid all-day bars included, are outlined in their event color with black text. Today's marker is a black outline.
  • Neutrals are black on white with color-scheme: light, so an app in dark mode still prints on paper. Event, now and background-event colors stay the theme's. With shadcn/ui or Material UI the event colors follow the app's dark variables, because those themes switch on a class rather than color-scheme.
  • Slot and hour lines, unavailable hatching, background layers and background events print as drawn (print-color-adjust: exact).
  • The now line is hidden.
  • The time grid, timeline and list stop scrolling, drop a height limit set in a layer, and their headers stop sticking, so every hour prints from the top. A height limit in your own unlayered CSS, on the calendar or its container, still cuts it; lift it in a @media print rule.
  • Month weeks, months in a year view, list days, timeline rows and events avoid breaking across pages.
  • A timeline prints at the width it lays out, so the page edge cuts a long range. Use landscape, a shorter range or a smaller minSlotWidth on the view.