carbon-components-svelte Documentation

Svelte implementation of the Carbon Design System

RAW Doc

Technical Documentation: carbon-design-system/carbon-components-svelte

โ„น๏ธ Provenance: Multi-Tier Ingestion ยท CodeWiki Reference

Overview (`README.md`)

carbon-components-svelte

NPM

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:

[Documentation](https://svelte.carbondesignsystem.com)

Installation

sh
# npm
npm i carbon-components-svelte

# pnpm
pnpm add carbon-components-svelte

# Yarn
yarn add carbon-components-svelte

# Bun
bun add carbon-components-svelte

Usage

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

js
// 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.

js
import "carbon-components-svelte/css/all.css";

Update the theme by setting the theme attribute on the html element. The default theme is "white".

html
<!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.

html
<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.

html
<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]
Using carbon-preprocess-svelte is optional and not a prerequisite for this library. It should be installed as a development dependency.

sh
# 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

diff
- 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.

diff
$ 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's plugins. Vite runs it on vite build only, and OptimizeCssPlugin runs 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.

js
optimizeCss({ propAware: true });
svelte
<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.

js
// 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 under src/) before the build. If a module outside content renders a Carbon component, or one of your rewritten wrappers, including through a re-export or a dynamic import(), 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: true to 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

js
// 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

js
// 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

js
// 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.

js
// 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.

sh
# npm
npm i carbon-icons-svelte

# pnpm
pnpm add carbon-icons-svelte

# Yarn
yarn add carbon-icons-svelte

# Bun
bun add carbon-icons-svelte
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.

sh
# npm
npm i carbon-pictograms-svelte

# pnpm
pnpm add carbon-pictograms-svelte

# Yarn
yarn add carbon-pictograms-svelte

# Bun
bun add carbon-pictograms-svelte
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

Apache 2.0

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 through
thumbnailPath, 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

sh
bun run thumbnails missing

It keys off the docs pages, not src/. Pages like ClickableTile live
inside another folder (src/Tile/), and folders like ListBox or
FormItem have no page. It also lists thumbnails with no page, except
tabs-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/:

sh
bun run thumbnails catalog

It 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 dashed
text-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, either
background-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 with
sheet, 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: with
icon-inverse it turns dark on g80, g90, and g100. A check on an
icon-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; see
RAW_COLOR_EXCEPTIONS in scripts/lib/thumbnails.ts).

To match what the component really renders, read its partial in
css/_<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

svg
<?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.

svg
<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

  1. Run bun run thumbnails missing to list missing thumbnails.
  2. For each one, read the page's frontmatter description: and its
    ## Basic example. Write down the one trait you'll draw before
    opening an editor.
  3. Render the sheet for its docs category
    (bun run thumbnails sheet --category <label>, labels from
    docs/src/component-categories.ts). Note what the neighbors look
    like so the new one doesn't.
  4. Copy the closest existing thumbnail as a starting point. The shared
    layout, tokens, and sizes come along for free.
  5. Draw it. Check colors against the component's SCSS partial. Keep
    drafts and alternatives in .context/drafts/, not thumbnails/.
    sheet takes paths too:
    bun run thumbnails sheet meter .context/drafts/meter-b.svg.
  6. Check the mechanics: bun run thumbnails audit <name>โ€ฆ and
    xmllint --noout thumbnails/<name>.svg.
  7. 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 built css/all.css
    (bun run build:css). Look for marks that vanish or invert against
    their fill.
  8. 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.
  9. Commit as chore: add thumbnail SVGs for <components>.
  10. Add a carbon.yml entry 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:

yaml
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/TableOfContents

name 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 to
Tabs#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

sh
# npm
npm i carbon-components-svelte

# pnpm
pnpm add carbon-components-svelte

# Yarn
yarn add carbon-components-svelte

# Bun
bun add carbon-components-svelte

2. 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.

javascript
// 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

svelte
<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:

javascript
import "carbon-components-svelte/css/all.css";

Then set the theme reactively in Svelte:

svelte
<script>
  let theme = "white"; // "white" | "g10" | "g80" | "g90" | "g100"

  $: document.documentElement.setAttribute("theme", theme);
</script>

Or statically in your HTML:

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:

sh
# 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

optimizeImports

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.

javascript
// 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:

javascript
// 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:

javascript
// 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:

javascript
// 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.

javascript
// 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.

sh
# npm
npm i carbon-icons-svelte

# pnpm
pnpm add carbon-icons-svelte

# Yarn
yarn add carbon-icons-svelte

# Bun
bun add carbon-icons-svelte
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.

sh
# npm
npm i carbon-pictograms-svelte

# pnpm
pnpm add carbon-pictograms-svelte

# Yarn
yarn add carbon-pictograms-svelte

# Bun
bun add carbon-pictograms-svelte
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.css exists. It is gitignored and built by bun setup; rebuild it with bun run build:css if your checkout is stale.
  • Chrome is installed. Bun finds it automatically; set BUN_CHROME_PATH to use another binary.
  • gh is authenticated, if you pass a tag. (A saved Markdown file works without it.)

Steps

  1. 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.

  2. Run it. Pass the release tag and the script fetches the notes with gh, or pass a saved Markdown file.

    sh
    bun 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 2

    Output goes to the gitignored release-screenshots/<name>.png. --jobs sets 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-cache forces a fresh render. A SKIP line means no svelte fence precedes that marker. Keep the markers in the body while drafting; strip them from the published notes when you replace them with images.

  3. 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.

  4. Attach the images by dragging the PNGs into the GitHub release editor, then delete the marker comments. Do not commit them.

How it works

  1. extractSnippets pairs each marker with its preceding svelte fence. The fences are written to node_modules/.cache/release-screenshots/<name>.svelte, under the repo so svelte resolves. GitHub returns CRLF line endings; they are normalized first (every fence regex fails otherwise).
  2. One Bun.build bundles all snippets as lazy modules chosen by ?s=<name>. A small plugin compiles .svelte files with svelte/compiler and aliases carbon-components-svelte to src/ and its css/ path to css/.
  3. Bun.serve serves the bundle from memory on a free port.
  4. Chrome and its views start while the bundle builds. Each view (--jobs of 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 and deviceScaleFactor through Emulation.setDeviceMetricsOverride, loads the page, waits for fonts and running animations, runs the recipe, then crops Page.captureScreenshot to the #app box.

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: 4 renders 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.
  • WebView has no element screenshot or scale option. release-screenshots.ts does 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 first cdp call. Each view also allows one cdp or evaluate call in flight, so do not Promise.all them.
  • Wait on animations, not timers. settle awaits document.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 quiet before 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 title tooltips are not rendered headlessly. RelativeTime uses title. Draw an equivalent element from the real title value (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 needs max-height: none on .bx--list-box--expanded .bx--list-box__menu. This changes the default rendering. Disclose it.
  • Element screenshots clip popups. Reserve space with min-height on #app; the clip is the #app box.
  • 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>, and export let all work as written in release notes.
  • Use a fixed viewport width (width + 64) so wrapping is reproducible. GridOverlay is pointer-events: none and sized to the viewport, so capture it at a real breakpoint width (1056 is lg).
  • 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 ---