Skip to content

Theming ​

The design system lets you switch themes or configure your own theme programmatically, including at runtime.

How it works ​

A theme in CDS 4 is a set of semantic tokens that are turned into CSS variables when the application starts:

ThemeDefinition (TS)  →  --cds-color-* / --cds-* (CSS variables)  →  component styles
                                     ↓
                          @theme (Tailwind CSS v4)  →  bg-surface-primary, rounded-200, …

The variables hold finished colors, exactly as they are written in the theme. The built-in themes write colors in oklch(). Opacity on top of a token is set with color-mix():

css
/* generated by CDS */
:root {
  --cds-color-brand-primary: oklch(57.4% 0.16 254.98);
  --cds-color-surface-primary: oklch(100% 0 0);
  --cds-spacing-400: 1rem;
  --cds-border-radius-200: 0.25rem;
}

/* usage */
.my-block {
  background: var(--cds-color-surface-primary);
  border: var(--cds-border-100) solid color-mix(in srgb, var(--cds-color-brand-primary) 40%, transparent);
  padding: var(--cds-spacing-400);
}

In CDS 3 and early CDS 4 builds the variables held RGB channels and a color was read as rgb(var(--cds-color-x)). That no longer works — see the migration guide.

Theme styles are injected into <style id="cds-theme-stylesheet"> and are recalculated reactively when the theme changes.

Enabling a theme ​

CDS 4 ships eight ready-made themes:

ThemeDescription
cdsLightlight theme, used by default
cdsDarkdark theme
b2bLightlight theme for B2B-Center applications
b2bDarkdark theme for B2B-Center applications
lumaLightlight Luma theme
lumaDarkdark Luma theme
promoLightlight promo theme
promoDarkdark promo theme

cdsLight is used by default — you don't need to enable it.

To switch to another theme, pass its name in the createCds options:

ts
import { createApp } from 'vue';
import '@central-design-system/components/styles';
import { createCds, components, directives } from '@central-design-system/components';

import App from './App.vue';

const cds = createCds({
  components,
  directives,
  theme: {
    defaultTheme: 'b2bLight'
  }
});

createApp(App).use(cds);

Theming options ​

ts
createCds({
  theme: {
    defaultTheme: 'cdsLight',
    prefix: 'cds',
    themes: {},
    variations: false,
    utilities: true,
    legacyClasses: false,
    scoped: false,
    scope: undefined,
    stylesheetId: 'cds-theme-stylesheet',
    cspNonce: undefined
  }
});
OptionTypeDefaultPurpose
defaultThemeThemeName | string'cdsLight'Theme active at startup
prefixstring'cds'Prefix of the generated CSS variables (--cds-color-*)
themesRecord<string, ThemeDefinition>built-in themesYour own themes; merged with the built-in ones
variationsfalse | { colors: string[]; lighten: number; darken: number }falseAuto-generation of lighter/darker color variations
utilitiesbooleantrueGeneration of background/text utility classes from theme tokens
legacyClassesbooleanfalseCDS 3 legacy classes (cds-bg-*, cds-text-*, cds-border-*)
scopedbooleanfalseDon't write the variables into :root
scopestring—Limit theme selectors to a subtree (:where(<scope>))
stylesheetIdstring'cds-theme-stylesheet'id of the <style> tag holding the theme
cspNoncestring—nonce for Content Security Policy

Creating your own theme ​

A custom theme is a ThemeDefinition object with the fields dark, colors and variables. You only need to specify the overrides: missing tokens are inherited from cdsLight / cdsDark.

A color can be written as hex, rgb() or oklch() — the value lands in the CSS variable exactly as written. CDS computes the contrasting on-* text and the variations for any of these formats.

ts
import { createApp } from 'vue';
import { createCds, components, directives } from '@central-design-system/components';

import type { ThemeDefinition } from '@central-design-system/components';

import App from './App.vue';

const myCustomGreenTheme: ThemeDefinition = {
  dark: false,
  colors: {
    // Main brand colors of the new theme
    'brand-primary': 'oklch(55.5% 0.1605 142.24)',
    'brand-primary-hover': '#3C9C2C',
    'brand-primary-active': '#50A946',
    'brand-primary-border': '#2C8926',
    'brand-primary-content': '#005005',
    'brand-secondary': '#DCF5D0',
    'brand-secondary-hover': '#A5DD98',
    'brand-secondary-active': '#97D283',
    'brand-secondary-border': '#BCE8AB',
    // Surfaces
    'page-primary': '#F2F8F0',
    'surface-secondary': '#E8F3E4'
  },
  // Variables: typography, spacing, radii, shadows
  variables: {
    'font-family': 'Roboto, sans-serif',
    'border-radius-200': '0.625rem'
  }
};

const cds = createCds({
  components,
  directives,
  theme: {
    defaultTheme: 'myCustomGreenTheme',
    themes: {
      myCustomGreenTheme
    }
  }
});

createApp(App).use(cds);

Every registered theme also gets a .cds-theme--<name> class — use it to enable the theme on an arbitrary part of the markup.

Token groups ​

colors — semantic colors:

  • brand-* — brand (primary, secondary and their hover / active / border / content)
  • surface-*, page-*, fill-* — surfaces and fills
  • content-* — text, icons, links
  • line-* — borders, dividers, focus and selection
  • status-{success,warning,progress,info,danger,indication}-* — statuses
  • other-* — additional accent palettes

variables — dimensions: spacing-*, font-size-*, font-line-height-*, font-weight-*, border-*, border-radius-*, shadow-{x,y,blur}-*, font-family, base-unit.

For the full token list of a specific theme, see the package sources: @central-design-system/components/src/themes/cdsLight.ts.

Switching the theme ​

You can change the theme at runtime through the useTheme composable:

vue
<script setup lang="ts">
import { useTheme } from '@central-design-system/components';

const theme = useTheme();

function toggleTheme(): void {
  theme.global.name.value = theme.global.current.value.dark ? 'cdsLight' : 'cdsDark';
}
</script>

<template>
  <cds-button @click="toggleTheme">Toggle theme</cds-button>
</template>

You can also change the theme of a single component by passing it the theme prop. Keep in mind that the theme applies not only to the component itself but to everything nested inside it:

vue
<template>
  <cds-notification theme="b2bLight">
    <!-- the button will use the b2bLight theme -->
    <cds-button>Button</cds-button>
  </cds-notification>
</template>

The theme prop is supported by container components and most visual components: CdsApp, CdsLayout, CdsPage, CdsSurface, CdsCard, CdsDrawer, CdsModal, CdsOverlay, CdsButton, CdsField, CdsTable and others.

Using theme tokens in your own code ​

Tailwind CSS v4 ​

Theme tokens are mapped into Tailwind's @theme, so utilities switch along with the theme automatically:

html
<div class="bg-surface-primary text-content-primary p-4 rounded-200">…</div>

Setup is described in Tailwind CSS v4 + CDS 4.

CSS / SCSS ​

scss
.my-card {
  background: var(--cds-color-surface-elevated);
  color: var(--cds-color-content-primary);
  padding: var(--cds-spacing-500);
  border-radius: var(--cds-border-radius-300);
}

When you override CDS component styles, put them into the cds-overrides layer — that way they are guaranteed to win over the library's styles:

scss
@layer cds-overrides {
  .cds-button {
    border-radius: var(--cds-border-radius-300);
  }
}

CDS 3 legacy classes ​

The CDS 3 utility classes (cds-bg-*, cds-text-*, cds-border-*) are off by default in CDS 4. To bring them back for the duration of a migration:

ts
import '@central-design-system/components/styles/legacy';

const cds = createCds({
  theme: { legacyClasses: true }
});

Disabling theming ​

Pass false to the theme option. This prevents the generation of CSS variables and theme classes and disables runtime theme switching — the <style> tag is not created at all.

ts
import { createCds } from '@central-design-system/components';

const cds = createCds({
  theme: false
});

With theming disabled, theme.isDisabled === true and themeClasses returns undefined.

Theme instance interface ​

useTheme() returns a ThemeInstance — access to the current theme and its settings.

ts
interface ThemeInstance {
  /** Whether theming is enabled (`false` with `theme: false`) */
  readonly isDisabled: boolean;

  /**
   * Raw object of the installed themes
   * Themes can be added and modified
   */
  readonly themes: Ref<Record<string, InternalThemeDefinition>>;

  /**
   * Name of the current theme
   * Inherited from the parent component
   */
  readonly name: Readonly<Ref<string>>;

  /**
   * Processed object of the current theme
   * Includes the automatically generated colors
   */
  readonly current: DeepReadonly<Ref<InternalThemeDefinition>>;
  readonly computedThemes: DeepReadonly<Ref<Record<string, InternalThemeDefinition>>>;

  /** Class of the current theme — `cds-theme--<name>` */
  readonly themeClasses: Readonly<ComputedRef<string | undefined>>;

  /** Generated CSS of the current theme */
  readonly styles: Readonly<ComputedRef<string>>;

  readonly global: {
    /** Name of the current global theme in the application (writable) */
    readonly name: Ref<string>;

    /**
     * Processed object of the current global theme in the application
     * Also available from `theme.computedThemes.value[theme.global.name.value]`
     */
    readonly current: DeepReadonly<Ref<InternalThemeDefinition>>;
  };
}

The theme instance is also available on the object returned by createCds() — handy outside of a component context:

ts
const cds = createCds({ theme: { defaultTheme: 'cdsLight' } });

cds.theme.global.name.value = 'cdsDark';