# Themes, tokens, Tailwind and dark mode

> Style the scheduler with --super-scheduler-* tokens, re-skin the accent so every tint follows, match your dark mode, and set up Tailwind v3 or v4 with the right layers.

Source: https://superscheduler.org/en/docs/theming/
Reviewed: 2026-10-07

Import the stylesheet once: it lives in @layer super-scheduler with zero-specificity selectors, so your unlayered CSS wins without !important. Re-skin with --super-scheduler-* tokens; component tokens work anywhere, but the accent family must be set on :root and .dark (site-wide, default colorScheme 'inherit') or on the control root with an explicit colorScheme. Dark mode follows an ancestor .dark class or data-theme attribute by default, and the operating system only with colorScheme: 'auto'.

SuperScheduler is styled entirely through CSS custom properties, classes and data attributes. You can make it match your product without forking the stylesheet and without `!important`. This guide describes the contract, the recipes that reliably reach every color, dark mode, per-slot classes, density, the unstyled mode and Tailwind. Most of it applies to Pro; the [last section](#lite) covers Lite.

## The CSS contract
Import the stylesheet once, next to your application's own CSS:

```css
@import 'super-scheduler/styles.css';
```

(In a bundler you can also write `import 'super-scheduler/styles.css'` in a module.) The file is a single `@layer super-scheduler` block of zero-specificity `:where()` selectors. Any unlayered rule of yours wins over it, whatever its specificity.

| Surface | Selector or prefix |
|---|---|
| Root | `.super-scheduler`, with `data-color-scheme`, `data-density`, `data-lod` and `data-unstyled` |
| Parts | `.super-scheduler__*` classes (`__event`, `__event-inner`, `__event-bar`, `__cell`, `__row-header-cell`, `__link`, ...) |
| Part markers | `[data-super-scheduler-part="event"]`, `"cell"`, `"event-meta"`, `"now-line"`, `"minimap"`, `"pane"`, ... |
| State | `data-selected`, `data-hovered`, `data-dragging`, `data-conflict`, `data-today`, `data-weekend`, `data-disabled`, `data-expanded`, ... |
| React content | `data-super-scheduler-slot`, `data-super-scheduler-slot-ready`, `data-super-scheduler-fallback` |
| Tokens | `--super-scheduler-*` |

Do not depend on chunk file names or generated identifiers. Geometry tokens such as `--super-scheduler-cell-width`, `-row-height` and `-row-header-width` are written by the engine: change sizes through options (`cellWidth`, `eventHeight`, `rowHeaderWidth`), not CSS.

## Tokens
Tokens come in three kinds, and where they are declared decides where you can override them.

- **Primitives**, declared on `:root`: `--super-scheduler-white`, `neutral-50` to `neutral-950`, `accent-50` to `accent-950`, `danger-400/600`, `warning-400/600`, `success-400/600`, `duration`, `ease`, `ease-spring`.
- **Semantic tokens**, declared for each scheme (light values on `:root` and light scopes, dark values on `.dark`, `[data-theme="dark"]` and `[data-color-scheme="dark"]`):

| Group | Tokens (all prefixed `--super-scheduler-`) |
|---|---|
| Surfaces and text | `surface`, `surface-raised`, `text`, `text-muted`, `on-accent` |
| Lines | `border`, `border-strong`, `grid-line`, `grid-break`, `row-line` |
| Accent | `accent-rgb`, `accent`, `accent-emphasis`, `accent-soft`, `accent-text`, `accent-border` |
| Status | `danger`, `danger-soft`, `warning`, `warning-soft`, `success` |
| Calendar shading | `today-bg`, `weekend-bg`, `non-business-bg`, `group`, `group-row`, `hatch` |
| Interaction | `hover-row`, `hover-cell`, `selection`, `selection-border`, `focus-ring`, `micro-bar`, `duration-bar` |
| Overlays and loading | `overlay`, `overlay-text`, `overlay-muted`, `overlay-subtle`, `overlay-border`, `skeleton-base`, `skeleton-highlight`, `shadow-1` to `shadow-3` |

- **Component tokens**, never declared: they are read with a fallback where they are used, so you can set them on any ancestor. Examples: `--super-scheduler-radius`, `-event-radius`, `-event-padding`, `-event-bg`, `-event-text`, `-event-border`, `-event-bar`, `-link`, `-link-hover`, `-now-line`, `-handle-target`, `-hover-*`, `-minimap-*`, `-pane-splitter`, `-zoom-hud-bg`.

## Re-skinning recipes that work
Because semantic tokens are declared per scheme, and a few are declared again on the control root, not every override reaches every tint. These four recipes were checked in the browser against the 0.1.0 stylesheet.

### Component tokens anywhere
Component tokens work on any ancestor, in every color scheme mode:

```css
.planning {
  --super-scheduler-radius: 8px;
  --super-scheduler-event-radius: 6px;
  --super-scheduler-event-padding: 2px 6px;
  --super-scheduler-link: #8b5cf6;
  --super-scheduler-now-line: #e11d48;
}
```

### Neutral colors on a wrapper
With the default `colorScheme: 'inherit'`, most semantic tokens can be overridden on a wrapper element: surfaces, text, lines, shading and selection tints.

```css
.planning {
  --super-scheduler-surface: #fbfaf7;
  --super-scheduler-border: #e6e1d6;
  --super-scheduler-weekend-bg: rgb(120 100 60 / 0.05);
}
.dark .planning {
  --super-scheduler-surface: #16140f;
  --super-scheduler-border: rgb(255 255 255 / 0.08);
}
```

Two limits: with an explicit `colorScheme` (`'light'`, `'dark'` or `'auto'`), the control root declares the semantic tokens itself and a wrapper no longer reaches them (use the last recipe). And `accent`, `duration-bar`, `grid-break`, `hatch`, `overlay-text`, `overlay-muted`, `overlay-subtle` and the scrollbar color are always declared on the root.

### A brand accent for the whole site
The accent family is computed from one channel token, `--super-scheduler-accent-rgb` (three space-separated numbers), plus a few primitives for the soft fills and text. Declare them unlayered on `:root` and on the dark scope your site uses:

```css
:root {
  --super-scheduler-accent-rgb: 10 140 80;
  --super-scheduler-accent-100: #e0f2e9; /* accent-soft: event fills */
  --super-scheduler-accent-200: #c9e6d6; /* border in light mode */
  --super-scheduler-accent-800: #04361f; /* accent-text, accent-emphasis */
}
:root.dark {
  --super-scheduler-accent-rgb: 80 200 140;
  --super-scheduler-accent-950: #0c2a1b; /* accent-soft in dark mode */
  --super-scheduler-accent-100: #d8f5e6; /* accent-text in dark mode */
}
```

Selection, today, focus ring, hover tints and micro-bars all follow, in both schemes. This recipe needs the default `colorScheme: 'inherit'`.

### A brand accent for one scheduler
To brand one scheduler, or to use an explicit `colorScheme`, put a class on the control root with `cssClass` and target it together with the scheme attribute:

```css
.brand[data-color-scheme='light'] {
  --super-scheduler-accent-rgb: 10 140 80;
  --super-scheduler-accent-100: #e0f2e9;
  --super-scheduler-accent-200: #c9e6d6;
  --super-scheduler-accent-800: #04361f;
}
.brand[data-color-scheme='dark'] {
  --super-scheduler-accent-rgb: 80 200 140;
  --super-scheduler-accent-950: #0c2a1b;
  --super-scheduler-accent-100: #d8f5e6;
}
.job--late {
  --super-scheduler-event-bg: var(--super-scheduler-danger-soft);
  --super-scheduler-event-border: var(--super-scheduler-danger);
  --super-scheduler-event-text: var(--super-scheduler-danger);
}
```

```tsx
// src/BrandedPlanner.tsx
import { useMemo } from 'react'
import { SuperSchedulerComponent } from 'super-scheduler'
import type { SchedulerProps, SuperScheduler } from 'super-scheduler'
import 'super-scheduler/styles.css'

type Job = { status: 'planned' | 'late' | 'done' }

// Stable objects: a new classNames or styles object is applied again on every render.
const CLASS_NAMES: SuperScheduler.SchedulerClassNames = {
  event: 'planning-event',
  rowHeaderCell: 'planning-row-header',
}
const STYLES: SuperScheduler.SchedulerStyles = {
  timeHeaderCell: { fontVariantNumeric: 'tabular-nums' },
}

// A class per state; the colors live in CSS, so they follow light and dark mode.
const statusClass: NonNullable<SchedulerProps['onBeforeEventRender']> = (args) => {
  const job = args.data as SuperScheduler.EventRenderData<Job>
  args.data.cssClass = `job job--${job.status}`
}

export function BrandedPlanner(props: {
  theme: 'light' | 'dark'
  resources: SuperScheduler.ResourceData[]
  jobs: SuperScheduler.EventData<Job>[]
}) {
  const events = useMemo(() => props.jobs.slice(), [props.jobs])
  return (
    <SuperSchedulerComponent
      // Extra class on the control root: the brand CSS targets `.brand[data-color-scheme=...]`.
      cssClass="brand"
      // Follows the application's own theme switch.
      colorScheme={props.theme}
      density="compact"
      classNames={CLASS_NAMES}
      styles={STYLES}
      onBeforeEventRender={statusClass}
      startDate="2026-10-01"
      days={31}
      scale="Day"
      resources={props.resources}
      events={events}
    />
  )
}
```
You should see green selection and event fills in light mode, their dark counterparts when `theme` is `'dark'`, and late jobs in the danger colors of each scheme. With `colorScheme: 'auto'`, write the same rules on `.brand` and on `.brand` inside `@media (prefers-color-scheme: dark)`.

> **Behavior:**
> Three overrides look right but do not work. Setting `--super-scheduler-accent` on an ancestor is shadowed by the root's own declaration. Setting `--super-scheduler-accent-rgb` on a wrapper changes the accent itself but none of the tints computed from it. Setting primitives such as `--super-scheduler-accent-100` on a wrapper changes nothing, because the semantic tokens already resolved them on `:root`.

## Color scheme and dark mode
`colorScheme` is written to the root as `data-color-scheme`:

| Value | Follows |
|---|---|
| `'inherit'` (default) | The nearest ancestor with the `dark` class or a `data-theme="dark"` / `data-theme="light"` attribute |
| `'light'`, `'dark'` | Forced; beats any ancestor |
| `'auto'` | The operating system's `prefers-color-scheme` |

Only `'auto'` reads the operating system. A light site on a dark system stays light with the default mode, which is usually what a product with its own theme switch wants. Tailwind's class-based dark mode (`<html class="dark">`) works with `'inherit'` as is.

Menus, bubbles and hover cards are mounted in `document.body`, outside the scheduler. They carry the scheduler's scheme and tokens, so they match it.

## Per-event colors and states
Simple colors need no CSS: `backColor`, `fontColor`, `borderColor`, `barColor` and `barBackColor` on the event data, or set in `onBeforeEventRender`. `SuperScheduler.ColorUtil.contrasting(color)` picks a readable text color.

For colors that should follow dark mode, prefer a class per state and component tokens, as in the recipe above: `cssClass` on the event, then `--super-scheduler-event-bg`, `-event-text`, `-event-border` and `-event-bar` in CSS. Events also expose interaction state as attributes: `data-hovered`, `data-dragging` and `data-resizing` on the event being changed, and `data-conflict` with `conflictHighlight`. Selected events need a class of your own in 0.1.0 (see [event selection](https://superscheduler.org/en/docs/trees-columns-selection/#event-selection)). The thin duration bar uses `--super-scheduler-duration-bar` globally or `--super-scheduler-event-bar` per event; `durationBarVisible: false` removes it.

## classNames and styles
`classNames` and `styles` add classes or inline styles to named parts. The slots are `root`, `scroll`, `corner`, `timeHeader`, `timeHeaderCell`, `rowHeader`, `rowHeaderCell`, `treeToggle`, `grid`, `row`, `cell`, `event`, `eventInner`, `eventBar`, `area`, `separator`, `link`, `selection`, `shadow`, `rectangle`, `crosshair`, `dragCard`, `tooltip`, `skeleton`, `empty`, `error` and `message`.

They accept Tailwind utilities as well as your own class names. Define both objects at module level or memoize them, and leave position, size, overflow and z-index to the engine: changing them breaks hit testing, sticky headers and virtualization. `cssClass` is a shortcut for one more class on the root.

## Density
`density` changes the default sizes without touching your options:

| Preset | Event height | Header height | Also |
|---|---|---|---|
| `'comfortable'` | 35 px | 30 px | Default look |
| `'compact'` | 28 px | 26 px | `2px 4px` event padding, 11 px event text |
| `'dense'` | 20 px | 22 px | `0 4px` padding, 10 px text, hides `[data-super-scheduler-part="event-meta"]` |

The preset sizes apply only while `eventHeight` and `headerHeight` keep their defaults; explicit values win. `control.update({ density: 'dense' })` relays out without remounting. Dense rows are hard targets on touch screens; offer them as a choice rather than a default on phones.

## Unstyled mode
For a fully custom look, set `unstyled` and do not import the stylesheet. The root gets `data-unstyled`, the engine keeps writing only structural inline styles (positions, sizes, stacking, sticky headers), and the live region for announcements stays visually hidden. Everything else is yours, including focus indicators, selection, hover and forced-colors support.

## Tailwind
The tokens are the contract; Tailwind is one way to consume them.

### Tailwind v3
`super-scheduler/tailwind` is a v3 preset. It adds colors (`bg-super-scheduler-surface`, `text-super-scheduler-text-muted`, `bg-super-scheduler-accent/20`, `text-super-scheduler-ink/60`, ...), radii (`rounded-super-scheduler`, `rounded-super-scheduler-event`), shadows (`shadow-super-scheduler-1` to `-3`), and `duration-super-scheduler` / `ease-super-scheduler`.

```ts
// tailwind.config.ts
import superSchedulerPreset from 'super-scheduler/tailwind'

// Tailwind v3. The preset adds colors, radii, shadows and easing utilities that read the
// --super-scheduler-* tokens (bg-super-scheduler-surface, rounded-super-scheduler-event...),
// so they follow the scheduler's light and dark values.
// In a CommonJS config: presets: [require('super-scheduler/tailwind')].
export default {
  content: ['./index.html', './src/**/*.{ts,tsx}'],
  darkMode: 'class',
  presets: [superSchedulerPreset],
}
```
Tailwind v3's preflight is unlayered, so it would beat the library's layered borders. Put preflight in a layer below the library:

```css
@layer tw-base, super-scheduler;

@import 'super-scheduler/styles.css';

@layer tw-base {
  @tailwind base;
}
@tailwind components;
@tailwind utilities;
```

### Tailwind v4
Tailwind v4 has no presets: map the tokens in `@theme inline`, after declaring the layer order. Inline mappings read the nearest token value, so nested dark themes work, and opacity modifiers such as `bg-super-scheduler-accent/20` work too.

```css
@layer theme, base, super-scheduler, components, utilities;

@import 'tailwindcss';
@import 'super-scheduler/styles.css';

@custom-variant dark (&:where(.dark, .dark *));

@theme inline {
  --color-super-scheduler-surface: var(--super-scheduler-surface);
  --color-super-scheduler-surface-raised: var(--super-scheduler-surface-raised);
  --color-super-scheduler-text: var(--super-scheduler-text);
  --color-super-scheduler-text-muted: var(--super-scheduler-text-muted);
  --color-super-scheduler-border: var(--super-scheduler-border);
  --color-super-scheduler-border-strong: var(--super-scheduler-border-strong);
  --color-super-scheduler-grid-line: var(--super-scheduler-grid-line);
  --color-super-scheduler-grid-break: var(--super-scheduler-grid-break);
  --color-super-scheduler-row-line: var(--super-scheduler-row-line);
  --color-super-scheduler-accent: var(--super-scheduler-accent);
  --color-super-scheduler-accent-emphasis: var(--super-scheduler-accent-emphasis);
  --color-super-scheduler-accent-soft: var(--super-scheduler-accent-soft);
  --color-super-scheduler-accent-text: var(--super-scheduler-accent-text);
  --color-super-scheduler-accent-border: var(--super-scheduler-accent-border);
  --color-super-scheduler-on-accent: var(--super-scheduler-on-accent);
  --color-super-scheduler-ink: rgb(var(--super-scheduler-ink-rgb));
  --color-super-scheduler-danger: var(--super-scheduler-danger);
  --color-super-scheduler-danger-soft: var(--super-scheduler-danger-soft);
  --color-super-scheduler-warning: var(--super-scheduler-warning);
  --color-super-scheduler-warning-soft: var(--super-scheduler-warning-soft);
  --color-super-scheduler-success: var(--super-scheduler-success);
  --color-super-scheduler-today-bg: var(--super-scheduler-today-bg);
  --color-super-scheduler-weekend-bg: var(--super-scheduler-weekend-bg);
  --color-super-scheduler-non-business-bg: var(--super-scheduler-non-business-bg);
  --color-super-scheduler-group: var(--super-scheduler-group);
  --color-super-scheduler-group-row: var(--super-scheduler-group-row);
  --color-super-scheduler-hover-row: var(--super-scheduler-hover-row);
  --color-super-scheduler-hover-cell: var(--super-scheduler-hover-cell);
  --color-super-scheduler-selection: var(--super-scheduler-selection);
  --color-super-scheduler-selection-border: var(--super-scheduler-selection-border);
  --color-super-scheduler-focus-ring: var(--super-scheduler-focus-ring);
  --color-super-scheduler-micro-bar: var(--super-scheduler-micro-bar);
  --color-super-scheduler-duration-bar: var(--super-scheduler-duration-bar);
  --color-super-scheduler-hatch: var(--super-scheduler-hatch);
  --color-super-scheduler-overlay: var(--super-scheduler-overlay);
  --color-super-scheduler-overlay-text: var(--super-scheduler-overlay-text);
  --color-super-scheduler-overlay-muted: var(--super-scheduler-overlay-muted);
  --color-super-scheduler-overlay-subtle: var(--super-scheduler-overlay-subtle);
  --color-super-scheduler-overlay-border: var(--super-scheduler-overlay-border);
  --color-super-scheduler-skeleton-base: var(--super-scheduler-skeleton-base);
  --color-super-scheduler-skeleton-highlight: var(--super-scheduler-skeleton-highlight);
  --radius-super-scheduler: var(--super-scheduler-radius, 12px);
  --radius-super-scheduler-event: var(--super-scheduler-event-radius, 8px);
  --shadow-super-scheduler-1: var(--super-scheduler-shadow-1);
  --shadow-super-scheduler-2: var(--super-scheduler-shadow-2);
  --shadow-super-scheduler-3: var(--super-scheduler-shadow-3);
  --ease-super-scheduler: var(--super-scheduler-ease);
  --ease-super-scheduler-spring: var(--super-scheduler-ease-spring);
}
```

## Lite theming
Lite (`super-scheduler-lite/styles.css`) has six tokens: `--super-scheduler-background`, `-text`, `-border`, `-header`, `-event` and `-focus`. They are declared on the `.super-scheduler-lite` element itself, so set them on that element, not on an ancestor:

```css
.bookings .super-scheduler-lite {
  --super-scheduler-event: #dcfce7;
  --super-scheduler-focus: #15803d;
}
.dark .bookings .super-scheduler-lite {
  --super-scheduler-background: #111827;
  --super-scheduler-text: #f3f4f6;
  --super-scheduler-border: #374151;
  --super-scheduler-header: #1f2937;
  --super-scheduler-event: #14532d;
}
```

Lite has no built-in dark scheme, so dark values are yours to declare. Its stylesheet is also layered, and it adapts borders and focus to forced colors. Per event, Lite accepts `backColor`, `fontColor` and `cssClass`.

## What your application owns
- Contrast of every color you introduce, in light, dark and high-contrast modes.
- The theme switch itself: where the `dark` class or `data-theme` goes, or which `colorScheme` you pass.
- Styles for custom content (HTML from hooks, React slots, hover cards): use the tokens so it follows the scheme.
- Checking your overrides after upgrades. Tokens and part selectors are the stable contract; internal class order and markup between parts are not.

## Related
→ https://superscheduler.org/en/examples/sports-club-courts/
→ https://superscheduler.org/en/examples/festival-stages/
→ https://superscheduler.org/en/examples/hotel-rooms/
- [Keyboard, accessibility and touch](https://superscheduler.org/en/docs/keyboard-accessibility-touch/#motion-contrast) for reduced motion, high contrast and forced colors.
- [React render slots and hover cards](https://superscheduler.org/en/docs/react-render-slots/) for the hover card tokens.
