Dark Mode

The scheduler ships two theme modes, light and dark. The active mode drives every surface: the timeline, resource sidebar, event blocks, dialogs, and context menus.

How the Theme Is Applied

Setting the theme writes data-df-theme="light" or data-df-theme="dark" onto the scheduler's wrapper element. The --df-color-* tokens are defined for that attribute, so everything inside re-themes from a single switch.

The same tokens are also defined for a .dark ancestor class, which means a host application that already toggles .dark on <html> re-themes the scheduler without any configuration.

Setting the Initial Theme

const app = useSchedulerApp({
  resources,
  events,
  theme: 'dark',
});
const app = useSchedulerApp({
  resources,
  events,
  theme: 'dark',
});
app = new SchedulerApp({
  resources: this.resources,
  events: this.events,
  theme: 'dark',
});
const app = createSchedulerApp({
  resources,
  events,
  theme: 'dark',
});

theme defaults to 'light'.

Switching at Runtime

MethodDescription
app.getTheme()Returns the current mode.
app.setTheme('light' | 'dark')Switches the active mode.
function ThemeToggle({ app }) {
  const { theme } = app.getState();

  return (
    <button onClick={() => app.setTheme(theme === 'light' ? 'dark' : 'light')}>
      {theme === 'dark' ? 'Light' : 'Dark'}
    </button>
  );
}

Following the System Preference

There is no built-in auto mode: theme accepts only 'light' or 'dark'. Read prefers-color-scheme yourself and forward it:

import { useEffect } from 'react';

function useSystemTheme(app) {
  useEffect(() => {
    const mql = window.matchMedia('(prefers-color-scheme: dark)');

    const apply = (e: MediaQueryListEvent | MediaQueryList) => {
      app.setTheme(e.matches ? 'dark' : 'light');
    };

    apply(mql);
    mql.addEventListener('change', apply);
    return () => mql.removeEventListener('change', apply);
  }, [app]);
}

Following a Host .dark Class

If your app already toggles .dark on <html>, the scheduler's colors follow automatically through the ancestor selector. Mirror it into app state as well when your own code reads app.getTheme():

useEffect(() => {
  const sync = () =>
    app.setTheme(
      document.documentElement.classList.contains('dark') ? 'dark' : 'light'
    );

  sync();
  const observer = new MutationObserver(sync);
  observer.observe(document.documentElement, {
    attributes: true,
    attributeFilter: ['class'],
  });
  return () => observer.disconnect();
}, [app]);

Portals and Dark Mode

Dialogs, context menus, and drag tooltips render into document.body under .df-portal, outside the scheduler wrapper. They inherit a host .dark class from <html>, but not the wrapper's data-df-theme attribute.

That distinction only matters for custom token overrides: target both selectors so portaled UI matches:

.df-scheduler-react,
.df-portal {
  --df-color-primary: #6366f1;
}

Per-resource Dark Colors

Resources can carry a separate palette for dark mode, which is used instead of colors when the theme is dark:

const resources = [
  {
    id: 'engineering',
    name: 'Engineering',
    style: {
      colors: { color: '#0ea5e9', textColor: '#082f49' },
      darkColors: { color: '#38bdf8', textColor: '#e0f2fe' },
    },
  },
];

On this page