Technical Documentation: carbon-design-system/carbon-components-svelte
โน๏ธ Provenance: Multi-Tier Ingestion ยท CodeWiki Reference
Overview (`README.md`)
carbon-components-svelte
Carbon Components Svelte is a Svelte component library that implements the Carbon Design System, an open source design system by IBM. Ship accessible, consistent, production-ready interfaces.
- 100+ components: from inputs to data tables
- 5 built-in themes: two light, three dark
- Fully typed TypeScript API: props, events, and slots
- WCAG 2.1 AA: keyboard and screen-reader ready
The Carbon Svelte ecosystem also includes:
- Carbon Icons Svelte: 2,700+ Carbon icons as Svelte components
- Carbon Pictograms Svelte: 1,500+ Carbon pictograms as Svelte components
- Carbon Charts Svelte: 25+ charts, powered by d3
- Carbon Preprocess Svelte: Svelte preprocessors and build plugins for Carbon
[Documentation](https://svelte.carbondesignsystem.com)
Installation
# npm
npm i carbon-components-svelte
# pnpm
pnpm add carbon-components-svelte
# Yarn
yarn add carbon-components-svelte
# Bun
bun add carbon-components-svelteUsage
Styling
Before importing components, you will need to first apply Carbon component styles. The Carbon Design System supports five themes (2 light, 3 dark).
- white.css: Default Carbon theme (light)
- g10.css: Gray 10 theme (light)
- g80.css: Gray 80 theme (dark)
- g90.css: Gray 90 theme (dark)
- g100.css: Gray 100 theme (dark)
- all.css: All five themes (White, Gray 10, Gray 80, Gray 90, Gray 100) using CSS variables
Each StyleSheet is compiled from Carbon v10 SCSS ([email protected], vendored) and this library's own partials in css.
The compiled CSS is generated from the following .scss files:
Import one theme once at the top-level entry point of your app, like index.js or src/+layout.svelte.
CSS StyleSheet
// White theme
import "carbon-components-svelte/css/white.css";
// Gray 10 theme
import "carbon-components-svelte/css/g10.css";
// Gray 80 theme
import "carbon-components-svelte/css/g80.css";
// Gray 90 theme
import "carbon-components-svelte/css/g90.css";
// Gray 100 theme
import "carbon-components-svelte/css/g100.css";
// All themes
import "carbon-components-svelte/css/all.css";Smaller CSS
Each precompiled StyleSheet contains the styles for every component. To ship only the styles your app uses, add optimizeCss to your production build. Set propAware: true to also drop the styles of prop values your app never passes; see prop-aware pruning.
The SCSS sources are not published. Compiling carbon-components v10 SCSS yourself misses styles for components and variants that only this library provides.
Dynamic theming
To switch themes at runtime, import the combined "all.css" StyleSheet instead of a single theme. It bundles all five themes and toggles between them through a theme attribute on the HTML element.
import "carbon-components-svelte/css/all.css";Update the theme by setting the theme attribute on the html element. The default theme is "white".
<!doctype html>
<html lang="en" theme="g10">
<body>
...
</body>
</html>Or programmatically switch between each of the five Carbon themes by setting the theme attribute on the HTML element.
<script>
let theme = "white"; // "white" | "g10" | "g80" | "g90" | "g100"
$: document.documentElement.setAttribute("theme", theme);
</script>Alternatively, use the Theme component to manage the theme reactively.
Importing components
Import components from carbon-components-svelte in the script tag of your Svelte file. Visit the documentation site for examples.
<script>
import { Accordion, AccordionItem } from "carbon-components-svelte";
</script>
<Accordion>
<AccordionItem title="Section 1" open> Content 1 </AccordionItem>
<AccordionItem title="Section 2"> Content 2 </AccordionItem>
<AccordionItem title="Section 3"> Content 3 </AccordionItem>
</Accordion>Refer to the documentation site for component API documentation.
Preprocessors & Plugins
carbon-preprocess-svelte is a collection of Svelte preprocessors and build plugins for Carbon. It trims build times and bundle size with three drop-in tools: faster compiles in development, leaner CSS and leaner JavaScript when you ship. Each works on its own.
[!NOTE]
Usingcarbon-preprocess-svelteis optional and not a prerequisite for this library. It should be installed as a development dependency.
# npm
npm i -D carbon-preprocess-svelte
# pnpm
pnpm add -D carbon-preprocess-svelte
# Yarn
yarn add -D carbon-preprocess-svelte
# Bun
bun add -D carbon-preprocess-svelte| Tool | Type | What it does |
|---|---|---|
optimizeImports |
Svelte preprocessor | Rewrites barrel imports to source paths, for faster dev and build times |
optimizeCss |
Build plugin | Removes unused Carbon styles from production CSS |
optimizeComponents |
Build plugin | Rewrites Carbon components for the props your app passes, removing code it never runs |
`optimizeImports`
optimizeImports is a Svelte preprocessor that rewrites barrel imports from Carbon components/icons/pictograms packages to their source Svelte code paths. This can significantly speed up development and production build compile times while preserving typeahead and autocompletion offered by integrated development environments (IDE) like VS Code.
The preprocessor optimizes imports from the following packages:
Before & After
- import { Button } from "carbon-components-svelte";
+ import Button from "carbon-components-svelte/src/Button/Button.svelte";
- import { Add } from "carbon-icons-svelte";
+ import Add from "carbon-icons-svelte/lib/Add.svelte";
- import { Airplane } from "carbon-pictograms-svelte";
+ import Airplane from "carbon-pictograms-svelte/lib/Airplane.svelte";`optimizeCss`
optimizeCss is a build plugin that removes unused Carbon styles at build time, often removing hundreds of kilobytes from production bundles. It runs on Vite, Rollup, and Rolldown. For Webpack and Rspack, use OptimizeCssPlugin.
[email protected] or greater is required.
$ vite build
Optimized assets/index-Doxsm5Yz.css
- Before: 557.37 kB
+ After: 46.16 kB (-91.72%)
dist/index.html 0.33 kB โ gzip: 0.23 kB
dist/assets/index-Doxsm5Yz.css 46.16 kB โ gzip: 6.12 kB
dist/assets/index-BoBcXTHi.js 69.61 kB โ gzip: 24.35 kB[!NOTE]
This is a plugin and not a Svelte preprocessor. Add it to your bundler'splugins. Vite runs it onvite buildonly, andOptimizeCssPluginruns in Webpack's production mode only. Under Rollup and Rolldown, add it to production builds only.
Prop-aware pruning
Set propAware: true to also remove styles for the prop values your app never passes.
optimizeCss({ propAware: true });<Button kind="tertiary">Save</Button>It reads literal props, constants and state nothing reassigns (including constants your own modules export), your own wrapper components, props forwarded with {...$$restProps} or ...rest, object and array literals, and your own {#if} and {#each}. A value it can't read keeps every variant: a reassigned variable, a writable store, bind:, a spread of anything but a literal or forwarded props, or data fetched at runtime.
Refer to the carbon-preprocess-svelte documentation for details.
`optimizeComponents`
optimizeComponents rewrites each Carbon component your app renders for the props it passes. Values that never change become literals, and branches that can't run are removed, along with child components only those branches render (a skeleton, a tooltip portal). It is the JavaScript counterpart of prop-aware CSS pruning and uses the same analysis. In typical apps it removes 18โ27% of the production JavaScript.
Your own wrapper components are rewritten the same way when every place that renders them is in content.
// vite.config.js
import { svelte } from "@sveltejs/vite-plugin-svelte";
import {
optimizeComponents,
optimizeCss,
optimizeImports,
} from "carbon-preprocess-svelte";
export default {
plugins: [
optimizeComponents(),
svelte({ preprocess: [optimizeImports()] }),
optimizeCss({ propAware: true }),
],
};| Bundler | Set-up | Runs on |
|---|---|---|
| Vite, SvelteKit, Astro | add optimizeComponents() to vite.plugins; it orders itself first |
vite build only, never vite dev |
| Rollup, Rolldown | list optimizeComponents() before the Svelte plugin |
production builds only (add it conditionally) |
| Webpack, Rspack | add new OptimizeComponentsPlugin() to plugins |
mode: "production" only |
- It analyzes the files matched by
content(default: source files undersrc/) before the build. If a module outsidecontentrenders a Carbon component, or one of your rewritten wrappers, including through a re-export or a dynamicimport(), the build fails instead of shipping wrong code. - Wrappers imported through an alias (
$lib/Card.svelte,#lib/Card.svelte) are resolved with the bundler's own resolver. - SvelteKit and Astro server and client builds are rewritten alike, so prerendered pages hydrate as before.
- Set
report: trueto print what each build rewrote.
Refer to the carbon-preprocess-svelte documentation for every option.
Configure your bundler
Add optimizeImports to your Svelte preprocessor and optimizeCss to your bundler plugins. See examples for full configurations.
Vite
// vite.config.js
import { svelte, vitePreprocess } from "@sveltejs/vite-plugin-svelte";
import { optimizeCss, optimizeImports } from "carbon-preprocess-svelte";
export default {
plugins: [
svelte({
preprocess: [vitePreprocess(), optimizeImports()],
}),
optimizeCss(),
],
};SvelteKit
// vite.config.js
import adapter from "@sveltejs/adapter-static";
import { sveltekit } from "@sveltejs/kit/vite";
import { vitePreprocess } from "@sveltejs/vite-plugin-svelte";
import { optimizeCss, optimizeImports } from "carbon-preprocess-svelte";
export default {
plugins: [
sveltekit({
preprocess: [vitePreprocess(), optimizeImports()],
adapter: adapter(),
}),
optimizeCss(),
],
};On SvelteKit 2, pass the same preprocess in svelte.config.js and the adapter under kit.
Rollup
// rollup.config.js
import svelte from "rollup-plugin-svelte";
import { optimizeCss, optimizeImports } from "carbon-preprocess-svelte";
const production = !process.env.ROLLUP_WATCH;
export default {
plugins: [
svelte({
preprocess: [optimizeImports()],
}),
production && optimizeCss(),
],
};Webpack
Rspack implements Webpack's plugin and loader APIs, so the same configuration works in rspack.config.mjs.
// webpack.config.mjs
import { OptimizeCssPlugin, optimizeImports } from "carbon-preprocess-svelte";
export default {
module: {
rules: [
{
test: /\.svelte$/,
use: {
loader: "svelte-loader",
options: {
preprocess: [optimizeImports()],
},
},
},
],
},
plugins: [new OptimizeCssPlugin()],
};Icons and pictograms
The icon and pictogram sets ship as separate packages of individual Svelte components. Each is optional, so install only what you need.
Icons
2,700+ icons designed for product UI, in four sizes (16, 20, 24, and 32 pixels), set with a single prop.
# npm
npm i carbon-icons-svelte
# pnpm
pnpm add carbon-icons-svelte
# Yarn
yarn add carbon-icons-svelte
# Bun
bun add carbon-icons-svelte<script>
import Add from "carbon-icons-svelte/lib/Add.svelte";
</script>
<Add size={24} />Pictograms
1,500+ illustrative pictograms for empty states, onboarding, hero sections, and feature callouts. Larger than icons, 64px by default.
# npm
npm i carbon-pictograms-svelte
# pnpm
pnpm add carbon-pictograms-svelte
# Yarn
yarn add carbon-pictograms-svelte
# Bun
bun add carbon-pictograms-svelte<script>
import Cloud from "carbon-pictograms-svelte/lib/Cloud.svelte";
</script>
<Cloud />Documentation for LLMs
Documentation is available in LLM-friendly plain text for use with coding assistants, plus a standalone Markdown document for every component. Append .md to any component's URL to read it.
- llms.txt: a component index where each entry links to its per-component Markdown doc, sized for model context windows.
- llms-full.txt: the full component documentation in a single plain-text file.
Examples
TypeScript support
Every component ships TypeScript definitions for its props, events, and slots (src/**/*.svelte.d.ts), generated from the component source by sveld.
Contributing
Refer to the Contributing guidelines.
[Changelog](CHANGELOG.md)
License
IBM Telemetry
This package uses IBM Telemetry to collect de-identified and anonymized metrics data in CI environments. By installing
this package as a dependency you are agreeing to telemetry collection. To opt out, see
Opting out of IBM Telemetry data collection.
For more information on the data being collected, please see the
IBM Telemetry documentation.
Overview (`thumbnails/README.md`)
Thumbnails
Every component with a docs page gets a 320ร180 SVG here, named after
the page in kebab case (ToggleButtonGroup.svx โtoggle-button-group.svg). carbon.yml points at these throughthumbnailPath, and they're the preview a catalog shows next to the
component name.
New components usually ship without one, and without the carbon.yml
entry that makes the catalog show it. Check both before each release.
Find what's missing
bun run thumbnails missingIt keys off the docs pages, not src/. Pages like ClickableTile live
inside another folder (src/Tile/), and folders like ListBox orFormItem have no page. It also lists thumbnails with no page, excepttabs-vertical.svg, which backs the tabs-vertical catalog entry
(TabsVertical is documented on the Tabs page). Any other thumbnail with
no page is for a component that doesn't exist; delete it.
missing doesn't read carbon.yml. catalog does, and exits non-zero
on any gap between it, the docs pages, and thumbnails/:
bun run thumbnails catalogIt lists docs pages with no asset, assets whose thumbnailPath file is
missing, thumbnails no asset uses, and names that aren't sentence case
or don't spell the asset's key or docs page (it would have caught
"Tootlip icon"). Assets without a name inherit it from carbon-styles
and skip the name checks. A page deliberately left out of the catalog
goes in UNCATALOGED_PAGES in scripts/lib/thumbnails.ts, with a line
here saying why. None are left out today.
scripts/thumbnails.ts has three more commands, used in the
recipe: audit checks the mechanical rules below, sheet
renders a contact sheet, and themes renders the files in all five
themes.
Design principles
A thumbnail is a glyph, not a screenshot. People see it at a fraction
of its size in a grid full of others, so it has one job: make the
component recognizable and tell it apart from its neighbors.
One idea per thumbnail. Pick the single trait that defines the
component and draw only that. Meter is a track with a fill in a status
color. It isn't three meters in three states with threshold ticks. If
you're listing features, cut. The first draft is almost always too
busy.
Abstract by default. Text is a flat bar, not words. Media is a
filled block. Icons appear only when they carry the meaning (bold,
italic, and underline in ToggleButtonGroup, chevrons on Carousel
controls).
Real text only when the text is the component. Link, Text,
RelativeTime, and Truncate show literal words, because "2 days ago"
says more than any shape could. Keep it to a short phrase. A list is
not its text: OrderedList is bars with numbers, not "Ordered List"
four times.
Distinguish from look-alikes. Before drawing, find the existing
thumbnail it could be mistaken for and pick the trait that separates
them. Meter and ProgressBar are both a labeled track. Meter's warning
yellow fill is the difference, and ProgressBar keeps the blue. The
likeliest look-alikes sit in the same docs category, so render that
category's sheet (bun run thumbnails sheet --category Layout).
Show invisible structure with dashed outlines. Some components are
about something you can't see: rows that aren't rendered
(VirtualList), padding that content breaks out of (Bleed). A dashedtext-disabled outline marks a boundary or a thing that isn't there.
Solid shapes are things that render.
Nothing cut off, no chrome. Draw every element whole. Leave out
scrollbars, window frames, and partial rows, even when the real
component has them. A clipped shape reads as a rendering bug at
thumbnail size.
Spend color on the point. Stay in greys and use one accent, eitherbackground-brand blue or a status color, on the element that carries
the idea: the selected tab, the pressed button, the meter fill. Two
accents compete.
Shadows mean "floats above". Only layers that float get a shadow:
menus, popovers, tooltips, modals, toasts. Draw it with the
elevation recipe so every floating layer casts the same
one.
Match the family. Separator rules are 1px border-subtle, the
same as Divider and DescriptionList. Text bars are 6px or 8px tall.
Keep stroke weights, bar heights, and spacing consistent with the
existing set so the grid reads as one system. Reuse the
visual vocabulary for states instead of
inventing a new cue.
Draw symbols at the weight of the bars. The set is flat fills.
A symbol drawn as an outlined illustration, or at twice the size of
the bars beside it, looks pasted in. The first LocalStorage draft had
a 48px stroked database cylinder and read as clip art; outlined
braces at the same weight as the key/value bars fit. Strokes are for
1px borders and dashed boundaries, not for drawing objects.
Fill the safe area. A lone 16px icon in the middle of the canvas
disappears in the grid. A thumbnail that is one control (CopyButton,
BadgeIndicator, Loading) scales it up; see the sizes under
Canvas and geometry.
Twins share one design. Components that come in a pair or family
look alike on purpose and differ in one place. LocalStorage and
SessionStorage are the same braces, with the session one inside a
browser tab. The skeleton family shares one shimmer. The tooltip
family shares one bubble and differs by trigger.
No cursors. A pointer or hand says "interactive" about everything,
so it says nothing. Show the component's own cue instead: an arrow
icon on ClickableTile, a chevron on ExpandableTile. The exception is
ContextMenu, whose idea is a menu that opens at the pointer.
When a symbol doesn't land, change direction. Don't nudge the
size or color of a symbol that reads wrong. Draft three or four
different directions in .context/, render them side by side withsheet, and pick one. Draft twins as pairs so you judge the
difference between them too.
Break a rule only when breaking it is the idea. ScrollGradient is
about content that runs past the edge, so its rows are cut off.
TagSet is about color variety, so it uses several. If the exception
isn't the component's one idea, the rule holds.
Canvas and geometry
| Rule | Value |
|---|---|
| Canvas | width="320px" height="180px" viewBox="0 0 320 180" |
| Safe area | roughly x 64โ256, y 28โ152 |
| Default content width | 168 (x 76โ244), centered |
| Text bars | height 8 for labels and values, 6 for body text |
| Fields | 32 tall, field-01 with a 1px border-strong bottom edge; label bar 8 above, 16 to the field |
| Menus and lists | rows 18โ22 apart; a highlighted row is a 22-tall border-subtle band |
| Buttons | 24 tall when secondary to the idea, 32 when they are the idea |
| Icons | 16px (scale(0.5)) inline; 20px for standalone arrows; 24โ28px inside a lone control |
| Lone control | about 56ร56, centered (CopyButton, BadgeIndicator) |
| Rings and spinners | radius 28, stroke 4 |
| Rules | height="1" rect, not a stroked line |
| 1px strokes | offset by 0.5 (x="76.5") so edges stay crisp |
| Coordinates | whole numbers, except the 0.5 stroke offset |
Center the composition both ways. Measure the bounding box of
everything you drew, not just the main shape.
Visual vocabulary
The same state should look the same in every thumbnail.
| Meaning | Draw it as | Seen in |
|---|---|---|
| Selected | 1px icon-primary outline on the tile, plus a filled control |
RadioTile, SelectableTile |
| Pressed | border-subtle fill with a 2px background-brand bottom bar |
ToggleButtonGroup |
| Current or active | 2px background-brand underline or fill |
Tabs, PaginationNav, PageHeader |
| Typing | 1ร14 icon-primary caret after the text bar |
ComboBox |
| Filter match | the start of each row's bar dark, the rest light | ComboBox |
| Open | floating panel under the field, chevron pointing up | Dropdown, MultiSelect |
| Not rendered, or a boundary | 1px dashed text-disabled outline |
VirtualList, Bleed, Breakpoint, FloatingPortal |
| Composed of parts | 1px dashed background-brand outline around each part |
ComposedModal |
| Loading | shimmer gradient from text-disabled to border-subtle |
Skeleton family |
| Stored data | outlined { } around key/value bars |
LocalStorage, SessionStorage |
| Status | support color on the icon and a 3px left edge | InlineNotification, ToastNotification |
| Focus | don't draw it |
Look-alike pairs and the trait that separates them:
| Pair | Separating trait |
|---|---|
| ComboBox / Dropdown | typing and filtered matches / a checked selection |
| ComboButton / MenuButton | a split action and trigger / one button with a menu |
| FloatingPortal / Popover | escapes a clipped container / caret on a trigger |
| RadioTile / SelectableTile | stacked radios, one selected / a row of checkboxes, several checked |
| ClickableTile / ExpandableTile / Tile | arrow / revealed section and chevron / neither |
| Modal / ComposedModal | plain / dashed outlines around header, body, and footer |
| Form / FluidForm | labels above fields / labels inside joined fields |
| Tooltip / TooltipIcon / TooltipDefinition / Toggletip | icon with a bubble above / icon button with a side label / underlined term / pressed trigger with a link inside |
| SkeletonText / SkeletonPlaceholder / SkeletonIcon | paragraph / block / small square |
| Meter / ProgressBar | status-colored fill / blue fill |
Color tokens
Every fill and stroke uses var(--cds-<token>, <hex>) so the
thumbnail follows the host theme. The hex is the white-theme value
and is what renders anywhere the variable isn't defined, including
GitHub and rsvg-convert. Reuse these pairs exactly:
| Token | Hex | Use for |
|---|---|---|
--cds-layer-02 |
#ffffff |
Raised surfaces, containers |
--cds-field-01 |
#f4f4f4 |
Fields, tiles, unpressed buttons |
--cds-border-subtle |
#e0e0e0 |
Rules, tracks, pressed or selected fills, media blocks |
--cds-text-disabled |
#c6c6c6 |
Secondary text bars, dashed outlines |
--cds-layer-accent-active |
#a8a8a8 |
Primary text bars |
--cds-border-strong |
#8d8d8d |
Strong borders, emphasized blocks |
--cds-text-secondary |
#525252 |
Headings, terms, labels with weight |
--cds-button-secondary |
#393939 |
Secondary buttons, dark selected segments |
--cds-icon-primary |
#161616 |
Icons, real text |
--cds-icon-inverse |
#ffffff |
Marks on icon-primary or inverse fills (checkbox checks, notification titles); flips with them |
--cds-text-on-color |
#ffffff |
Labels and icons on blue, status, or secondary-button fills; white in every theme |
--cds-background-brand |
#0f62fe |
The one accent: selected, active, links |
--cds-support-success |
#198038 |
Success status |
--cds-support-warning |
#f1c21b |
Warning status |
--cds-support-error |
#da1e28 |
Error status |
--cds-background-inverse |
#393939 |
Tooltips, notifications, inverse surfaces |
--cds-text-inverse |
#ffffff |
Text bars on inverse surfaces |
The thumbnails theme only when they're inlined into a page that
defines the tokens. Loaded through `` or on GitHub, an SVG is its
own document, so every var() falls back to the hex.
Pick tokens by what they sit on, because some flip in dark themes and
some don't. icon-inverse, layer-02, and background-inverse flip;text-on-color stays white. A label on a blue button is text-on-color: withicon-inverse it turns dark on g80, g90, and g100. A check on anicon-primary checkbox is icon-inverse, so the two flip together.
bun run thumbnails audit fails on any other token or a mismatched
hex. Raw colors fail too, except in thumbnails whose idea is literal
color (tag, tag-set, theme, user-avatar-group; seeRAW_COLOR_EXCEPTIONS in scripts/lib/thumbnails.ts).
To match what the component really renders, read its partial incss/_<component>.scss and map the Sass token to the closest row
above. For example, _meter.scss fills the warning state with$support-warning, which is --cds-support-warning.
File template
<?xml version="1.0" encoding="UTF-8"?>
<svg width="320px" height="180px" viewBox="0 0 320 180" version="1.1" xmlns="http://www.w3.org/2000/svg">
<title>component-name</title>
<g id="component-name" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<rect id="label" fill="var(--cds-text-disabled, #c6c6c6)" x="76" y="74" width="60" height="8"></rect>
</g>
</svg>Give each part an id that names what it is (track, fill,selected). The ids make the file reviewable without rendering it.
Draw plain shapes. No , , or xlink:href: they're
how design-tool exports reuse geometry, and they make the file harder
to read and edit. Keep each file under 8 KB; a bigger one is usually
outlined text or exported cruft.
Elevation
A floating layer is two rects with the same geometry: a black one
under the shadow filter, then the surface on top.
<defs>
<filter id="component-name-shadow" filterUnits="userSpaceOnUse" x="0" y="0" width="320" height="180">
<feOffset dy="2" in="SourceAlpha" result="offset"></feOffset>
<feGaussianBlur stdDeviation="3" in="offset" result="blur"></feGaussianBlur>
<feColorMatrix in="blur" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.3 0"></feColorMatrix>
</filter>
</defs>
<rect id="shadow" fill="black" filter="url(#component-name-shadow)" x="100" y="48" width="120" height="44"></rect>
<rect id="surface" fill="var(--cds-layer-02, #ffffff)" x="100" y="48" width="120" height="44"></rect>The filter region covers the whole canvas on purpose. A region in
percentages of a short shape is too small for the blur and clips it
into a smear, which is what broke tooltip-icon.svg. The fill="black"
under a filter is the one raw color audit allows.
Recipe
- Run
bun run thumbnails missingto list missing thumbnails. - For each one, read the page's frontmatter
description:and its## Basicexample. Write down the one trait you'll draw before
opening an editor. - Render the sheet for its docs category
(bun run thumbnails sheet --category <label>, labels fromdocs/src/component-categories.ts). Note what the neighbors look
like so the new one doesn't. - Copy the closest existing thumbnail as a starting point. The shared
layout, tokens, and sizes come along for free. - Draw it. Check colors against the component's SCSS partial. Keep
drafts and alternatives in.context/drafts/, notthumbnails/.sheettakes paths too:bun run thumbnails sheet meter .context/drafts/meter-b.svg. - Check the mechanics:
bun run thumbnails audit <name>โฆandxmllint --noout thumbnails/<name>.svg. - Check themes:
bun run thumbnails themes <name>โฆrenders the
files in white, g10, g80, g90, and g100 to.context/thumbnails-themes.png. It needs a builtcss/all.css
(bun run build:css). Look for marks that vanish or invert against
their fill. - Review:
bun run thumbnails sheet <name>โฆwrites.context/thumbnails-sheet.png. Seeing the new ones side by side
catches inconsistent weight and spacing that a single render hides.
Check each against the principles above, and cut whatever isn't
the one idea. - Commit as
chore: add thumbnail SVGs for <components>. - Add a
carbon.ymlentry for each new thumbnail, in a separate
commit (chore: add <components> to carbon.yml). See
Catalog entries.
For a cleanup that shouldn't change the picture (renaming ids,
inlining ``, swapping a raw hex for its token), render every
touched file before and after and compare the pixels. Visual review
misses one-pixel shifts and dropped shadows; a diff doesn't.
Catalog entries
carbon.yml lists one asset per component under assets:, keyed by the
thumbnail name and kept in alphabetical order. A component that Carbon
also ships inherits its name and description from carbon-styles, so its
entry only adds thumbnailPath, externalDocsUrl, and the demo link. A
Svelte-only component spells everything out:
table-of-contents:
name: Table of contents
status: stable
type: component
platform: web
framework: svelte
thumbnailPath: './thumbnails/table-of-contents.svg'
demoLinks:
- type: other
name: Documentation
action: link
url: https://svelte.carbondesignsystem.com/components/TableOfContentsname is the page name in Carbon's sentence case: "Toggle button group",
not "Toggle Button Group". Acronyms stay upper case ("UI shell header",
"PIN code input"), and Carbon's one-word names stay one word
("Multiselect", "Toggletip"). A component documented inside
another page links to that page's section, as tabs-vertical links toTabs#vertical. Check it with bun run thumbnails catalog, which also fails if the file
doesn't parse.
Gotchas
| Gotcha | Why it matters |
|---|---|
Prefix every id that's defined in <defs> or referenced through url(#โฆ) or href with the component name (scroll-gradient-clip) |
A page that inlines several thumbnails shares one id namespace, so a bare clip or shadow id makes one SVG pick up another's definition. sheet rasterizes each file first for the same reason. |
Draw floating layers without a transform |
A userSpaceOnUse filter region is measured in the element's own coordinates. On a rotated or translated element the canvas-sized region moves with it and can clip the shadow. tooltip-icon.svg had a rotated bubble path for this reason; it's now a plain rect and a caret polygon. |
Don't keep drafts in thumbnails/ |
A cleanup glob like rm thumbnails/v*-*.svg matches virtual-list.svg too. Drafts go in .context/drafts/. |
The root <g> sets stroke="none" |
Outlined shapes need an explicit stroke and stroke-width, or they render with no border. |
Real text: outline it, don't use <text> |
<text> falls back to Helvetica or Arial wherever Plex isn't installed. Link, Text, Truncate, RelativeTime, and the numerals in OrderedList and PaginationNav are outlined paths instead: IBM Plex Sans Regular (from @ibm/plex-sans, drawn with Python fontTools), 12px by default, baseline at y 95 and centered on x 160 for a single phrase. Round coordinates to one or two decimals and keep phrases short; outlined glyphs cost about 500 B each against the 8 KB budget. |
| Icons are 32-unit paths | Copy the `` from node_modules/carbon-icons-svelte/lib/.svelte and apply transform="translate(x, y) scale(0.5)" for a 16px icon. |
sheet needs rsvg-convert |
brew install librsvg. No ImageMagick needed. |
| A new thumbnail doesn't add a catalog entry | The catalog only shows what carbon.yml lists, so a thumbnail with no entry never appears. Add the entry in its own commit; see Catalog entries. |
Content Overview (`docs/content/overview.md`)
A complete Svelte component library that implements the IBM Carbon Design System. Ship accessible, consistent, production-ready interfaces.
- 100+ components โ from inputs to data tables
- 5 built-in themes โ two light, three dark
- Fully typed TypeScript API โ props, events, and slots
- WCAG 2.1 AA โ keyboard and screen-reader ready
Quick start
Install the library, pick a theme, and render your first component. Three steps to a running app, plus customization options below.
1. Install the package
# npm
npm i carbon-components-svelte
# pnpm
pnpm add carbon-components-svelte
# Yarn
yarn add carbon-components-svelte
# Bun
bun add carbon-components-svelte2. Apply a theme stylesheet
Import one precompiled Carbon theme. The Carbon Design System supports five themes (2 light, 3 dark): White, Gray 10, Gray 80, Gray 90, Gray 100.
Import this once at the top-level, like index.js or src/+layout.svelte.
// White theme
import "carbon-components-svelte/css/white.css";
// Gray 10 theme
import "carbon-components-svelte/css/g10.css";
// Gray 80 theme
import "carbon-components-svelte/css/g80.css";
// Gray 90 theme
import "carbon-components-svelte/css/g90.css";
// Gray 100 theme
import "carbon-components-svelte/css/g100.css";
// All themes (for dynamic theming)
import "carbon-components-svelte/css/all.css";3. Import a component
<script>
import { Button } from "carbon-components-svelte";
</script>
<Button>Hello Carbon</Button>Explore the full component index.
Dynamic theming
To switch themes at runtime, import the combined all.css stylesheet instead of a single theme. It bundles all five themes and toggles between them through a theme attribute on the HTML element.
Import the stylesheet once, at the top-level entry point of your app:
import "carbon-components-svelte/css/all.css";Then set the theme reactively in Svelte:
<script>
let theme = "white"; // "white" | "g10" | "g80" | "g90" | "g100"
$: document.documentElement.setAttribute("theme", theme);
</script>Or statically in your HTML:
<html theme="white"></html>Or use the Theme component to manage the theme reactively.
Faster builds, smaller bundles
The fast path is enough to build. carbon-preprocess-svelte trims build times and bundle size with three drop-in tools: faster HMR in development, and leaner CSS and JavaScript when you ship. Each works on its own.
Add carbon-preprocess-svelte as a dev dependency:
# npm
npm i -D carbon-preprocess-svelte
# pnpm
pnpm add -D carbon-preprocess-svelte
# Yarn
yarn add -D carbon-preprocess-svelte
# Bun
bun add -D carbon-preprocess-svelteoptimizeImports
Rewrites barrel imports to direct source paths, dramatically cutting cold build and HMR times.
optimizeCss
Tree-shakes unused Carbon CSS at build time, often removing hundreds of kilobytes from production bundles. In typical apps it removes 60โ80% of the stylesheet. Set propAware: true to also remove styles for prop values your app never passes, which removes over 80%.
optimizeComponents
Rewrites each Carbon component your app renders for the props it passes, removing branches and child components that can never run. In typical apps it removes 18โ27% of the production JavaScript. Your own wrapper components are rewritten the same way when every place that renders them is in content. It runs on production builds only, and fails the build if a module outside its content globs renders a Carbon component or a rewritten wrapper, including through a re-export or a dynamic import(). For Webpack and Rspack, use OptimizeComponentsPlugin.
// vite.config.js
import { svelte } from "@sveltejs/vite-plugin-svelte";
import {
optimizeComponents,
optimizeCss,
optimizeImports,
} from "carbon-preprocess-svelte";
export default {
plugins: [
optimizeComponents(),
svelte({ preprocess: [optimizeImports()] }),
optimizeCss({ propAware: true }),
],
};Configure your bundler
Add optimizeImports to your Svelte preprocessor and optimizeCss to your bundler plugins.
Vite:
// vite.config.js
import { svelte, vitePreprocess } from "@sveltejs/vite-plugin-svelte";
import { optimizeCss, optimizeImports } from "carbon-preprocess-svelte";
export default {
plugins: [
svelte({
preprocess: [vitePreprocess(), optimizeImports()],
}),
optimizeCss(),
],
};SvelteKit:
// vite.config.js
import adapter from "@sveltejs/adapter-static";
import { sveltekit } from "@sveltejs/kit/vite";
import { vitePreprocess } from "@sveltejs/vite-plugin-svelte";
import { optimizeCss, optimizeImports } from "carbon-preprocess-svelte";
export default {
plugins: [
sveltekit({
preprocess: [vitePreprocess(), optimizeImports()],
adapter: adapter(),
}),
optimizeCss(),
],
};On SvelteKit 2, pass the same preprocess in svelte.config.js and the adapter under kit.
Rollup:
// rollup.config.js
import svelte from "rollup-plugin-svelte";
import { optimizeCss, optimizeImports } from "carbon-preprocess-svelte";
const production = !process.env.ROLLUP_WATCH;
export default {
plugins: [
svelte({
preprocess: [optimizeImports()],
}),
production && optimizeCss(),
],
};Webpack:
Rspack implements Webpack's plugin and loader APIs, so the same configuration works in rspack.config.mjs.
// webpack.config.mjs
import { OptimizeCssPlugin, optimizeImports } from "carbon-preprocess-svelte";
export default {
module: {
rules: [
{
test: /\.svelte$/,
use: {
loader: "svelte-loader",
options: {
preprocess: [optimizeImports()],
},
},
},
],
},
plugins: [new OptimizeCssPlugin()],
};Icons and pictograms
The icon and pictogram sets ship as separate packages of individual Svelte components. Both are professionally designed, original iconography crafted to complement the scale and grid of Carbon components. Each is optional, so install only what you need.
Icons
2,700+ icons designed for product UI, in four sizes (16, 20, 24, and 32 pixels), set with a single prop.
# npm
npm i carbon-icons-svelte
# pnpm
pnpm add carbon-icons-svelte
# Yarn
yarn add carbon-icons-svelte
# Bun
bun add carbon-icons-svelte<script>
import Add from "carbon-icons-svelte/lib/Add.svelte";
</script>
<Add size={24} />Pictograms
1,500+ illustrative pictograms for empty states, onboarding, hero sections, and feature callouts. Larger than icons, 64px by default.
# npm
npm i carbon-pictograms-svelte
# pnpm
pnpm add carbon-pictograms-svelte
# Yarn
yarn add carbon-pictograms-svelte
# Bun
bun add carbon-pictograms-svelte<script>
import Cloud from "carbon-pictograms-svelte/lib/Cloud.svelte";
</script>
<Cloud />Documentation for LLMs
Documentation is available in LLM-friendly plain text for use with coding assistants, plus a standalone Markdown document for every component. Append .md to any component's URL to read it.
- llms.txt โ A component index where each entry links to its per-component Markdown doc, sized for model context windows.
- llms-full.txt โ The full component documentation in a single plain-text file.
Collection
The Carbon Svelte collection includes packages for icons, pictograms, and data visualization:
- Carbon Components Svelte โ 100+ components โ GitHub
- Carbon Icons Svelte โ 2,700+ icons โ GitHub
- Carbon Pictograms Svelte โ 1,500+ pictograms โ GitHub
- Carbon Charts Svelte โ 25+ charts, powered by d3 โ GitHub
- Carbon Preprocess Svelte โ Svelte preprocessors and build plugins โ GitHub
Package.Json (`docs/package.json`)
{
"private": true,
"scripts": {
"dev": "bun run format-component-api && bun run --parallel index-docs dev:vite",
"dev:vite": "vite dev",
"build": "bun run format-component-api && bun run --parallel build:index-docs build:sitemap && bun run build:md-docs && bun run build:vite",
"index-docs": "bun scripts/index-docs.ts",
"build:index-docs": "bun scripts/index-docs.ts",
"build:md-docs": "bun scripts/generate-md-docs.ts",
"build:sitemap": "bun scripts/generate-sitemap.ts",
"format-component-api": "bun scripts/format-component-api.ts",
"build:vite": "vite build",
"typecheck:examples": "bun scripts/typecheck-examples.ts",
"preview": "vite preview"
},
"devDependencies": {
"@roxi/routify": "^3.6.4",
"@sveltejs/vite-plugin-svelte": "^7.3.1",
"@types/mdast": "^4.0.4",
"@types/prismjs": "^1.26.5",
"@types/unist": "^3.0.3",
"carbon-icons-svelte": "^13.15.0",
"carbon-pictograms-svelte": "^13.20.0",
"clipboard-copy": "^4.0.1",
"estree-walker": "^3.0.3",
"github-slugger": "^2.0.0",
"mdsvex": "^0.12.8",
"minisearch": "^7.1.0",
"prettier": "^3.9.9",
"prettier-plugin-svelte": "^4.1.1",
"prismjs": "^1.30.0",
"rehype-slug": "^6.0.0",
"svelte": "^5.57.1",
"unist-util-visit": "^5.1.0",
"vite": "^8.3.1"
},
"type": "module"
}
Release Screenshots (`docs/release-screenshots.md`)
Release screenshots
Recipe for turning the and markers in a GitHub release body into 4x PNGs. It was first run for v0.113.0. Run it as part of the post-release checklist.
Each marker follows a fenced svelte snippet. The goal is for the image to be what that snippet renders, so every snippet is compiled verbatim, bundled with the repo's own src/ and css/all.css, and captured in your installed Chrome. It is a Bun script with no dependencies beyond what the repo already has: no Playwright, no Vite, and nothing written into e2e/. All 15 v0.113.0 shots take about 3.5 seconds cold and well under a second when cached.
This is for running locally. It needs Chrome (or Chromium) installed, and Bun 1.4 or newer for Bun.WebView.
Prerequisites
css/all.cssexists. It is gitignored and built bybun setup; rebuild it withbun run build:cssif your checkout is stale.- Chrome is installed. Bun finds it automatically; set
BUN_CHROME_PATHto use another binary. ghis authenticated, if you pass a tag. (A saved Markdown file works without it.)
Steps
Add a recipe for each new marker in
scripts/lib/release-screenshot-recipes.ts(see Recipes). A snippet with no recipe is captured as mounted at 420px wide, which is right for static components.Run it. Pass the release tag and the script fetches the notes with
gh, or pass a saved Markdown file.shbun run release:screenshots vX.Y.Z # all bun run release:screenshots vX.Y.Z meter box # some bun run release:screenshots notes.md --scale 2 --out /tmp/shots --jobs 2Output goes to the gitignored
release-screenshots/<name>.png.--jobssets how many Chrome views capture at once (default 4, or fewer on a smaller machine). Unchanged shots are reused from a cache (see Caching);--no-cacheforces a fresh render. ASKIPline means nosveltefence precedes that marker. Keep the markers in the body while drafting; strip them from the published notes when you replace them with images.Check the output. Compare each image against the marker's description, not just "it rendered". Previews of 4x images can exceed viewer size limits; downscale a copy to look, never the original:
sips -Z 900 in.png --out /tmp/in.png.Attach the images by dragging the PNGs into the GitHub release editor, then delete the marker comments. Do not commit them.
How it works
extractSnippetspairs each marker with its precedingsveltefence. The fences are written tonode_modules/.cache/release-screenshots/<name>.svelte, under the repo sosvelteresolves. GitHub returns CRLF line endings; they are normalized first (every fence regex fails otherwise).- One
Bun.buildbundles all snippets as lazy modules chosen by?s=<name>. A small plugin compiles.sveltefiles withsvelte/compilerand aliasescarbon-components-sveltetosrc/and itscss/path tocss/. Bun.serveserves the bundle from memory on a free port.- Chrome and its views start while the bundle builds. Each view (
--jobsof them, in one Chrome) pulls shot names off a shared queue, so shots run side by side. A single view allows one CDP call at a time, which is why parallelism comes from views, not from overlapping calls on one. For each shot, a view sets the viewport anddeviceScaleFactorthroughEmulation.setDeviceMetricsOverride, loads the page, waits for fonts and running animations, runs the recipe, then cropsPage.captureScreenshotto the#appbox.
The browser is always spawned fresh (url: false). Without that, Bun attaches to a Chrome you already have open with remote debugging, and macOS prompts you to allow it.
Caching
Each shot is cached under node_modules/.cache/release-screenshots/png, keyed on a hash of its snippet, recipe (options and act source), --scale, the library (src/ and css/all.css), and the capture scripts themselves. Edit one recipe and only that shot re-renders. If every shot is cached, Chrome never starts (about 0.2s). Change anything in src/, rebuild the CSS, or edit the scripts and everything re-renders.
Set volatile: true on a recipe whose output depends on the clock, like relative-time, so it is always re-rendered.
Recipes
Set per-shot options in recipes:
| Option | Use |
|---|---|
width |
#app width in CSS px (default 420). Wider for tables and page headers. |
viewport |
Capture the whole viewport instead of #app. For fixed-position overlays such as GridOverlay; also zeroes #app padding. |
volatile |
Never cache this shot. |
act(view) |
Interaction to run before capture. |
Helpers for act, from scripts/lib/release-screenshots.ts:
| Helper | Does |
|---|---|
css(v, text) |
Inject a stylesheet. Use it to reserve room for popups (#app{min-height:...}). |
tag(v, selector, regex, nth) |
Find an element by text, aria-label, or title, and return a selector v.click can use. |
quiet(v, blur) |
Move the mouse away, and blur the active element unless blur is false. |
settle(v) |
Wait for fonts, running animations, and two frames. Runs before and after every act already. |
waitFor(v, selector) |
Resolve once the selector matches. |
v.click(selector, { modifiers }) |
Real click with an actionability wait. |
v.cdp(method, params) |
Any CDP command, for hover and anything else WebView lacks. |
Patterns that came up:
| Marker says | Do |
|---|---|
| Pressed or selected state | v.click(await tag(...)), then quiet(v). |
| Menu or popover open | Click the trigger, css a min-height on #app, then quiet(v, false) to keep the focused menu item. |
| Video: "move to the next slide" and similar | Capture the end state of the described interaction. Screenshots stand in for videos. |
| Virtualized list | Find the scroller (scrollHeight > clientHeight) and set scrollTop to itemHeight * row. |
| flatpickr | Click the input, wait for .flatpickr-calendar.open, then tag the day cells (exclude .prevMonthDay and .nextMonthDay). Pass modifiers: ["Shift"] for shift-click. A min-height of about 470. |
| Tooltip on hover | See the gotcha below. |
| Several sibling components | Mount as written. If they stack too tightly, lay them out in a row with css and say so in the PR. |
Gotchas
- 4x, PNG, no downscaling.
deviceScaleFactor: 4renders text and edges natively at 4x; it is not an upscale. The files are small (7 to 90 KB) because the content is flat color and text. WebViewhas no element screenshot or scale option.release-screenshots.tsdoes both over CDP. Round the clip height up (Math.ceil), or Chrome crops a CSS pixel and the image no longer matches an element-screenshot render.cdp()needs a session.navigate("about:blank")once before the firstcdpcall. Each view also allows onecdporevaluatecall in flight, so do notPromise.allthem.- Wait on animations, not timers.
settleawaitsdocument.getAnimations(), so menus and calendars are captured after their open animation, with no fixed sleeps. - Focus rings and hover. A click leaves the clicked element focused, and the mouse stays over it. Call
quietbefore capture unless the marker asks for focus. Menus are the exception: keep the focus ring on the first item, which is what the component does when opened. - Native
titletooltips are not rendered headlessly.RelativeTimeusestitle. Draw an equivalent element from the realtitlevalue (see the recipe) and say in the PR that it is simulated. - Capped menus.
MultiSelect's menu stops at about 5.5 rows, so a marker that names every group header needsmax-height: noneon.bx--list-box--expanded .bx--list-box__menu. This changes the default rendering. Disclose it. - Element screenshots clip popups. Reserve space with
min-heighton#app; the clip is the#appbox. - Do not change the markup to look better. Layout-only CSS on
#app(width, flex row) is fine. Anything that edits the snippet or the component defeats the point; flag every deviation when you hand the images over. - Svelte 5 compiles these snippets in legacy mode, so
let:,slot=,<svelte:fragment slot>, andexport letall work as written in release notes. - Use a fixed viewport width (
width + 64) so wrapping is reproducible.GridOverlayispointer-events: noneand sized to the viewport, so capture it at a real breakpoint width (1056 islg). - Timestamps differ between runs.
RelativeTime's tooltip shows the current time minus five minutes. That is the only expected diff when re-running a release.
Tsconfig.Json (`docs/tsconfig.json`)
{
"extends": "../tsconfig.json",
"compilerOptions": {
"allowImportingTsExtensions": true,
"paths": {
"carbon-components-svelte": ["../src"],
"carbon-components-svelte/tokens": ["../src/tokens/index.js"],
"carbon-components-svelte/": ["../"],
"": ["./"]
}
},
"include": ["src//*", "scripts//", ".ts"]
}
Public Robots (`docs/public/robots.txt`)
User-agent: *
Allow: /
Sitemap: https://svelte.carbondesignsystem.com/sitemap.xml
--- METRICS ---
- Files Extracted: 7
- Estimated Token Budget: ~13702 tokens
- Canonical Reference: https://codewiki.google/github.com/carbon-design-system/carbon-components-svelte