Skip to content

Tree-shaking ​

CDS 4 is published as an ESM bundle with its module structure preserved: every component is a separate chunk with its own CSS next to it. That lets the bundler (Vite, webpack, Rollup) drop everything your application doesn't use.

For this to work, follow two rules:

  1. Register only the components you actually need in createCds.
  2. Include the base styles selectively rather than the whole styles.css.

Below is how to do that and how much it saves.

How it works ​

  • The ESM bundle is built with chunkSplitPlugin({ strategy: 'unbundle' }) — the library's module structure is preserved and components are not glued into a single file.
  • package.json declares sideEffects, so the bundler knows the library's JS modules are pure and safe to remove.
  • During the build, each component's chunk gets an import of its own CSS appended. That's why import { CdsButton } pulls in exactly CdsButton.css, not the styles of the entire library.

No special build or plugin is required — ordinary named imports are enough.

Selective component registration ​

Pass only the components you use to createCds:

ts
import { createApp } from 'vue';

import '@central-design-system/components/styles/common.css';
import { createCds, CdsButton, CdsCard, CdsIcon } from '@central-design-system/components';

import App from './App.vue';

const cds = createCds({
  components: { CdsButton, CdsCard, CdsIcon }
});

createApp(App).use(cds).mount('#app');

Components registered this way are available in templates globally — just as with full registration:

html
<template>
  <CdsCard>
    <CdsButton>Click me</CdsButton>
  </CdsCard>
</template>

Important: createCds does not register components on its own. The set is always defined by the caller. That's deliberate: if the factory referenced the full component list, the bundler could not remove a single one of them.

Alternative: a local import in an SFC ​

If you need a component in one or two places, global registration is optional — import it right inside the component:

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

<template>
  <CdsButton>Click me</CdsButton>
</template>

You still need createCds in the entry point — it supplies themes, locales, icons and breakpoints through provide. But you don't have to pass components to it at all.

Full registration ​

When the set of components is not known in advance (internal admin panels, sandboxes, documentation), it's easier to register everything:

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

const cds = createCds({ components, directives });

Such a call disables component tree-shaking: the components namespace import keeps the whole library in the bundle.

Styles ​

Component styles are included automatically along with the components themselves. The only thing you have to include manually is the base layer — reset, fonts, CSS layer order and global element styles:

ts
import '@central-design-system/components/styles/common.css';

The base layer is mandatory: it's where the @layer order is declared, and the correct cascade of component styles depends on it.

Take common.css, not core.css: core.css has no typography, so headings (h1–h6) keep browser sizes instead of the CDS scale.

Available options:

ImportSizeWhat's inside
styles/common.css~9 KBThe recommended base: core + typography + grid utilities
styles/core.css~2 KBLayers, fonts and reset only — no typography
styles/tailwind.css~12 KBCDS tokens for Tailwind CSS v4
styles/legacy.css~253 KBCompatibility with legacy CDS 3 markup

Don't use @central-design-system/components/styles in a tree-shaking scenario. It's a ~556 KB aggregate containing the CSS of every component in the library. It's convenient for a quick start, but it completely defeats selective registration.

Color tokens (--cds-color-* and the rest) are not in the CSS files — they are generated and injected into the document by createCds at runtime, based on the active theme. There is nothing extra to include.

Directives ​

Directives (ClickOutside, Intersect, Touch) are named exports and are registered just as selectively:

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

const cds = createCds({
  components: { /* ... */ },
  directives: { ClickOutside }
});

The library's own components don't need you to register directives — the ones that use ClickOutside, Intersect and Touch internally (CdsOverlay and everything built on it: CdsSelect, CdsMenu, CdsTooltip, CdsSlideView, input fields) import them themselves and register them locally. Pass directives to createCds only for your own markup — v-click-outside, v-intersect, v-touch in the application's templates.

In 4.0.0-alpha.0: local registration inside components didn't work, and the CdsSelect / CdsMenu menus wouldn't close on an outside click unless directives was passed to createCds. If you can't upgrade, add directives: { ClickOutside, Touch } to the registration.

How much it saves ​

Measurements on Vite 5 (production, minified, styles in a separate file, Vue included in the bundle, the base common.css not counted):

ScenarioJSJS (gzip)CSSCSS (gzip)
Full registration (components, directives)625 KB203 KB208 KB24.6 KB
createCds + 3 components196 KB62.6 KB27.6 KB4.1 KB
createCds + 1 component162 KB51.4 KB12.6 KB1.9 KB
1 component without createCds63 KB23.4 KB12.6 KB1.9 KB

The numbers depend on which components exactly you include: composite components (CdsTable, CdsAutocomplete, CdsDatePicker) pull in more dependencies than atoms do.

What isn't tree-shaken ​

About 99 KB is the core that createCds always includes: themes, locales, icons, breakpoints (useDisplay), goTo and the defaults system. That volume doesn't depend on the number of components — it is exactly the difference between the "1 component without createCds" (63 KB) and "createCds + 1 component" (162 KB) rows.

If CDS is used in a couple of places and you don't need the global subsystems, you can skip createCds entirely. But then there are no themes, no locales and no defaults, so that mode only suits pinpoint embeds.

How to verify the result ​

bash
npx vite-bundle-visualizer
bash
pnpm dlx vite-bundle-visualizer
bash
yarn dlx vite-bundle-visualizer
bash
bunx vite-bundle-visualizer

What to look for:

  • the bundle should contain no chunks of components missing from your createCds;
  • if an "extra" component shows up, it is most likely imported transitively (CdsSelect pulls in CdsMenu and CdsList, for instance) — that's normal;
  • if the whole library ended up in the bundle, look for components or import * as among your imports.

Common mistakes ​

A namespace import instead of a named one. Any import * as from the library keeps all of it:

ts
// ❌ pulls in every component
import * as cdsComponents from '@central-design-system/components';

// ✅
import { CdsButton } from '@central-design-system/components';

Importing icon packs through iconsets. The iconsets object references all packs at once, so it is not tree-shaken: the bundle gets the JS of every pack (including cds — 46 KB) and, since UnoCSS scans them itself, the CSS of all five packs (≈370 KB instead of ≈70 KB). Import the pack through its subpath:

ts
// ❌ pulls in every pack
import { iconsets } from '@central-design-system/components';
const aliases = iconsets.remix.aliases;

// ✅ remix only
import { remix } from '@central-design-system/components/iconsets/remix';
const aliases = remix.aliases;

Including the style aggregate. @central-design-system/components/styles contains the CSS of every component — use styles/common.css.

Your own barrel re-export. A src/ui/index.ts file that re-exports dozens of CDS components kills tree-shaking if you import everything from it. Import components directly from the library.

A bundler without ESM support. The CJS bundle (dist/cjs) and the IIFE build are not tree-shakable by design. Make sure your bundler resolves the import export condition — Vite, webpack 5 and Rollup do it by default.