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:
- Register only the components you actually need in
createCds. - 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.jsondeclaressideEffects, 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 exactlyCdsButton.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:
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:
<template>
<CdsCard>
<CdsButton>Click me</CdsButton>
</CdsCard>
</template>Important:
createCdsdoes 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:
<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:
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:
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:
| Import | Size | What's inside |
|---|---|---|
styles/common.css | ~9 KB | The recommended base: core + typography + grid utilities |
styles/core.css | ~2 KB | Layers, fonts and reset only — no typography |
styles/tailwind.css | ~12 KB | CDS tokens for Tailwind CSS v4 |
styles/legacy.css | ~253 KB | Compatibility with legacy CDS 3 markup |
Don't use
@central-design-system/components/stylesin 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:
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 theCdsSelect/CdsMenumenus wouldn't close on an outside click unlessdirectiveswas passed tocreateCds. If you can't upgrade, adddirectives: { 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):
| Scenario | JS | JS (gzip) | CSS | CSS (gzip) |
|---|---|---|---|---|
Full registration (components, directives) | 625 KB | 203 KB | 208 KB | 24.6 KB |
createCds + 3 components | 196 KB | 62.6 KB | 27.6 KB | 4.1 KB |
createCds + 1 component | 162 KB | 51.4 KB | 12.6 KB | 1.9 KB |
1 component without createCds | 63 KB | 23.4 KB | 12.6 KB | 1.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
npx vite-bundle-visualizerpnpm dlx vite-bundle-visualizeryarn dlx vite-bundle-visualizerbunx vite-bundle-visualizerWhat 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 (
CdsSelectpulls inCdsMenuandCdsList, for instance) — that's normal; - if the whole library ended up in the bundle, look for
componentsorimport * asamong your imports.
Common mistakes
A namespace import instead of a named one. Any import * as from the library keeps all of it:
// ❌ 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:
// ❌ 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.