Skip to content

Switching themes

Two themes are interchangeable when they define the same keys. Your app references keys, so any theme with the same keys renders it.

The bundled themes all satisfy ThemeContract, the keys of the library defaults. Use it to check your own themes:

import { defineConfig } from 'yarcl/define';
import defaults from 'yarcl/defaults';
import type { ThemeContract } from 'yarcl/themes';
export const midnight = defineConfig({
...defaults,
colors: { … },
sizes: { … },
}) satisfies ThemeContract;

A missing key (say, no lg size) is a type error, so the theme can’t silently break a screen that uses size="lg". Extra keys are allowed.

For your own design system, define your own contract the same way: a type listing the keys your app uses.

The simplest approach: one theme per build. Point the config file at the theme you want:

src/yarcl.config.ts
export { compact as default } from 'yarcl/themes';

To build the same app with several themes (white-label products, per-customer builds), keep one config file per theme and choose it with an environment variable in the plugin:

vite.config.ts
yarcl({ config: `src/themes/${process.env.THEME ?? 'default'}.ts` })

Remember the matching paths entry in tsconfig.json; with a shared contract, any of the theme files works for type-checking.

applyTheme from yarcl/css switches the whole app to another theme without a rebuild:

import { applyTheme, resetTheme } from 'yarcl/css';
import { themes } from 'yarcl/themes';
applyTheme(themes.editorial);
// later
resetTheme(); // back to the build-time config

It does two things:

  1. Replaces the styles. It runs generateCss, the same generator the Vite plugin uses at build time, and puts the result in a <style> element that overrides the build-time stylesheet. Dialogs, menus and toasts switch too.
  2. Switches the defaults. Components read defaults, per-component components defaults and heading levels from the applied theme, and re-render. A theme with components: { Button: { radius: 'rounded' } } gets pill buttons, and one with defaults: { density: 'compact' } gets compact tables.

The theme must define the keys your app uses, because prop types still come from the build-time config. The bundled themes share the defaults’ keys (the contract above), so any of them can replace another.

Pass onWarning to receive contrast warnings, and use useConfig() from yarcl when your own components need the active theme’s values:

applyTheme(theme, { onWarning: (message) => console.warn(message) });

This is how the theme playground and the demo on the home page work. generateCss is exported from yarcl/css as well, if you only need the stylesheet.

Every theme has light and dark values built in. The page follows the OS setting, and color-scheme switches it:

document.documentElement.style.colorScheme = 'dark';

Everything is a CSS variable, so a region can override values without a new theme:

.promo {
--yarcl-color-primary: light-dark(#be185d, #f9a8d4);
--yarcl-color-primary-on: #fff;
--yarcl-focus-color: var(--yarcl-color-primary);
}