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():
/* 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:
| Theme | Description |
|---|---|
cdsLight | light theme, used by default |
cdsDark | dark theme |
b2bLight | light theme for B2B-Center applications |
b2bDark | dark theme for B2B-Center applications |
lumaLight | light Luma theme |
lumaDark | dark Luma theme |
promoLight | light promo theme |
promoDark | dark promo theme |
cdsLightis used by default — you don't need to enable it.
To switch to another theme, pass its name in the createCds options:
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
createCds({
theme: {
defaultTheme: 'cdsLight',
prefix: 'cds',
themes: {},
variations: false,
utilities: true,
legacyClasses: false,
scoped: false,
scope: undefined,
stylesheetId: 'cds-theme-stylesheet',
cspNonce: undefined
}
});| Option | Type | Default | Purpose |
|---|---|---|---|
defaultTheme | ThemeName | string | 'cdsLight' | Theme active at startup |
prefix | string | 'cds' | Prefix of the generated CSS variables (--cds-color-*) |
themes | Record<string, ThemeDefinition> | built-in themes | Your own themes; merged with the built-in ones |
variations | false | { colors: string[]; lighten: number; darken: number } | false | Auto-generation of lighter/darker color variations |
utilities | boolean | true | Generation of background/text utility classes from theme tokens |
legacyClasses | boolean | false | CDS 3 legacy classes (cds-bg-*, cds-text-*, cds-border-*) |
scoped | boolean | false | Don't write the variables into :root |
scope | string | — | Limit theme selectors to a subtree (:where(<scope>)) |
stylesheetId | string | 'cds-theme-stylesheet' | id of the <style> tag holding the theme |
cspNonce | string | — | 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.
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,secondaryand theirhover/active/border/content)surface-*,page-*,fill-*— surfaces and fillscontent-*— text, icons, linksline-*— borders, dividers, focus and selectionstatus-{success,warning,progress,info,danger,indication}-*— statusesother-*— 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:
<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:
<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:
<div class="bg-surface-primary text-content-primary p-4 rounded-200">…</div>Setup is described in Tailwind CSS v4 + CDS 4.
CSS / 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:
@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:
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.
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.
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:
const cds = createCds({ theme: { defaultTheme: 'cdsLight' } });
cds.theme.global.name.value = 'cdsDark';