If you're not sure what to build or how to approach a change, file an issue before opening a PR.
Codebase:
Use Bun as the package manager and task runner. Run package scripts with bun <script> and one-off binaries with bunx <bin>.
Fork the repo and clone your fork:
git clone <YOUR_FORK>
cd carbon-components-svelteSet the original repository as the upstream:
git remote add upstream git@github.com:carbon-design-system/carbon-components-svelte.git
# verify that the upstream is added
git remote -vbun setupThis installs root and docs dependencies and generates TypeScript definitions and docs/src/COMPONENT_API.json.
Component documentation lives in docs/. The site uses Vite, Routify 3, Svelte 5, and MDsveX. The Vite config resolves carbon-components-svelte to the repository root, so edits under src/ show up in the docs without a separate package link.
To preview the docs site locally:
cd docs
bun devThe site serves at http://localhost:5173/ (or the next available port).
Conventions used across src/. Follow them in new and changed code.
Comments:
- Do not add trivial comments. If a comment restates what the code already says, delete it or improve the code instead.
- Reserve comments for non-obvious behavior: Svelte reactivity edge cases, ARIA placement, workarounds for Carbon v10 gaps. See loop guards in
ContentSwitcher.svelteand the duplicate-dispatch guard inCheckbox.svelte.
Patterns:
- Use
functiondeclarations in<script>. Declare handlers, defaults, and logic as functions rather than inline in markup. Named functions give stable references for default prop values (seedefaultShouldFilterinComboBox.svelte). - Avoid
afterUpdate. It runs after every DOM update and is easy to loop in Svelte 5. Prefer$:reactive statements with guards, event handlers,onMount, ortick()for DOM reads. Legacy code may still useafterUpdatefor scroll-sync or measurement; do not add new uses without a strong reason. - When moving a
dispatch(...)fromafterUpdateto$:, timing changes.afterUpdateran after the DOM commit;$:runs during the flush. A throwing handler can abort the rest of that flush. Update internalprevstate first, thentick().then(() => dispatch(...)), and snapshot values for the callback (const next = value). SeeFileUploader.svelte, the storage components, andTheme.svelte. Keep cancelable, user-driven dispatches synchronous. - Reset cached positional state (scroll offset, highlighted index, measured sizes) reactively when its source collection changes, not just on open/close. A value cached against the old list, such as a scroll position into pre-filter results, silently points past the end of the new one. Use a
$:guard comparing against the previous length or identity. - Put component-level JSDoc first (
@restProps,@slot,@template), then allexport letprops (each with its own JSDoc), then imports, then local logic. SeeButton.svelteandBox.svelte. - Keep small, component-specific helpers inline in the
<script>block. Extract tosrc/utils/only when the logic is shared across components or complex enough to unit-test in isolation. Seedebounce.jsandisOutsideClick.js. Token primitives such asText.svelteandBox.sveltemap props to utility classes and inline styles directly; their themeable rules live incss/_type.scss,css/_box.scss, and related partials. - Forward DOM events with bare
on:click/on:focus(no handler) on the underlying element (Button.svelte). - Interpolate attribute values with Svelte's attribute syntax, not template literals:
id="{treeId}-{id}-subtree", notid={`${treeId}-${id}-subtree`}. Keep template literals only when a value needs nested quotes or logic the shorthand can't express (seearia-labelinPinCodeInput.svelte). - Compound components use
setContext/getContextwithcarbon:keys (CheckboxGroup.svelte). - Default element IDs use
ccs-${Math.random().toString(36)}. - Key
{#each}blocks, for example(item.id ?? index)(seeRecursiveList.svelte). - Put shared logic in
src/utils/, for exampledebounce.jsandisOutsideClick.js. Prefer pure, DOM-free functions for layout, geometry, and state-decision math (seevirtualize.js). They are unit-testable in isolation, so edge cases get covered once in a util test instead of through expensive component renders. - ComboBox, Dropdown, and MultiSelect share listbox behavior (virtualization, keyboard navigation, outside-click) through
src/utils/. When you change shared menu behavior, apply and test the change in all three. The per-component wiring is parallel but not abstracted. - Use Carbon v10 markup:
bx--BEM classes in templates; SCSS patches use$prefixand tokens. See Custom styles. - Do not add themeable styles in per-component
<style>blocks (see Custom styles). - Use the legacy Svelte 5 API. Components use
export let,$:, andcreateEventDispatcher. Do not introduce runes ($state,$derived,$effect) unless the project explicitly migrates. - Bind per-iteration values once with
{@const}inside{#each}blocks, then reuse them across the markup instead of recomputing inline. SeeisSelected/isExpanded/rowClassValueinDataTable.svelteandactualIndexinComboBox.svelte. - Use
Set/Mapfor membership and id lookups instead of.includes()/.find(). The O(1) lookup matters in components that can hold large datasets. See theselectedRowIdsSet/nonSelectableRowIdsSetsets inDataTable.svelteand theitemsByIdmap inComboBox.svelte. - Gate expensive lookups by state. When an O(n) array computation is only needed in a certain state (on open, on hover), compute it imperatively in that state rather than in an always-on reactive statement or derivation. See
filteredItems = open ? items.filter(...) : []inComboBox.svelte. - Do not clobber external props. Never write internal or measured values back into an exported prop. A consumer may
bind:to a prop to control behavior, and overwriting it silently breaks that control. Measure into an internal fallback and fall back to it only when the public prop is unset. SeemeasuredMaxHeight/measuredPaddinginExpandableTile.svelteand theshowMoreLesshandling inCodeSnippet.svelte. This is the prop-facing counterpart to theafterUpdateguidance above. - Name booleans and predicates with an
isprefix. Useis<Verb>/is<Adjective>for boolean state and predicate functions. See theisOutsideClickutil,isSelected/isExpandedinDataTable.svelte, andisFluidinTextInput.svelte. - Prefer enums over booleans for props. When a prop selects among mutually exclusive variants, or the set of variants may grow, use a string-union prop instead of several boolean flags. It keeps states exclusive and extensible. See
kindandsizeinButton.svelte. Keep booleans for genuinely binary, independent toggles such asdisabledandopen. - Apply classes with the
class:directive, not string concatenation or template literals. Useclass:bx--name={true}for classes that are always present andclass:bx--name--modifier={condition}for conditional ones. This keeps each class on its own line and the toggling logic readable. SeeTextInput.svelte:class:bx--form-item={true}(always) alongsideclass:bx--text-input-wrapper--inline={inline}(conditional). - Forward
$$restPropsto the most important element. When a component has a clear primary element with the most customizable props, such as the<input>or<button>, spread{...$$restProps}onto it so consumers can set attributes likename,placeholder, ordata-*on the element they care about. See the<input>inTextInput.svelte. Only when there is no such element does it go to the wrapper (top-level element), as inTile.svelte. - Name slots in camelCase. No dashes: a hyphenated slot name will not map to a Svelte 5 Snippet. Use
labelChildren, notlabel-children. - Mirror the shadowed prop in the slot name. When a slot overrides the content a prop renders, name it after that prop. Svelte forbids a slot from matching an exported prop name exactly, so suffix it with
Children: thelabelChildrenslot shadows thelabelTextprop. See<slot name="labelChildren">inTextInput.svelte. - Set dynamic styles with the
style:directive, not inlinestylestrings or template literals:style:left="{x}px". SeeContextMenu.svelteand the thumb position inSlider.svelte.
The {#if skeleton} early-return pattern is being phased out. Follow it only when maintaining existing skeleton code.
API, docs, and workflow:
- Never hand-edit generated files.
src/**/*.svelte.d.ts,src/index.d.ts, anddocs/src/COMPONENT_API.jsonare produced by sveld and are gitignored. - Document the public API in JSDoc. Cover every exported prop, event, slot, and rest-props target. See Typing with JSDoc.
- Update component docs when appropriate. New props, features, and behavior changes need matching
.svxupdates. See Component documentation. - Use
$$restProps+@restPropsfor passthrough attributes (seeSearch.svelte). - Scope lint and tests to what you changed. See Checks and Unit tests.
Create a topic branch from master. Keep your PR focused and your branch current with upstream master.
git checkout -b new-featurePut each component in this layout:
src/Component
│
└───Component.svelte // main component
└───ComponentSkeleton.svelte // Skeleton component (if applicable)
└───index.js // export componentsType definitions are generated by sveld. See Typing with JSDoc.
Types live in JSDoc comments, not TypeScript inside .svelte files. sveld (via bun build:docs) reads those comments and writes:
src/**/*.svelte.d.tsandsrc/index.d.ts(consumer types)docs/src/COMPONENT_API.json(docs site API tables)
The auto-generated Props / Typedefs / Slots / Events tables at the bottom of each docs page come from COMPONENT_API.json. JSDoc is the source of truth for API tables; the .svx file covers usage examples and prose.
| Tag | Purpose |
|---|---|
@type |
Prop type |
@template |
Generic type params (supports defaults: @template {ComboBoxItem<any>} [Item=ComboBoxItem<any>]) |
@typedef / @property |
Item/shape types, event payloads |
@bindable writable / @bindable readonly |
Two-way and element refs |
@event |
Dispatched custom events (with payload type) |
@slot |
Named/default slot props |
@restProps |
Where $$restProps land |
@extends |
Inherit props from another component (skeleton wrappers) |
@default, @example |
Docs metadata |
Reference ComboBox.svelte for @template, @typedef, @event, @slot, and typed props. For generics in plain JS, see src/utils/debounce.js.
/**
* Specify the size of the component
* @type {"sm" | "default" | "lg"}
*/
export let size = "default";Whenever you add, remove, or retype an exported prop, slot, or event (including its JSDoc @type), regenerate and verify:
bun build:docs # sveld: types + COMPONENT_API.json
bun test:types # svelte-check on tests/bun setup already runs build:docs once. Repeat build:docs only when the component API changes.
File an issue first.
If you add a new component, export it from src/index.js:
export { CopyButton } from "./CopyButton";
export { ComboBox } from "./ComboBox";
+ export { FixedComboBox } from "./FixedComboBox";
export {
ComposedModal,
ModalHeader,
ModalBody,
ModalFooter,
} from "./ComposedModal";See Component documentation for the docs checklist.
Update documentation when the change is user-facing:
- New component: add a new page
- New prop, event, or slot: document usage (JSDoc covers the API table; add an example if non-obvious)
- New feature or variant: add or extend an example section
- Notable behavioral change: update existing prose/examples so they stay accurate
Skip doc updates for internal refactors with no API or behavior change.
| Path | Purpose |
|---|---|
docs/src/pages/components/{Component}.svx |
Main component page (for example Button.svx): prose + examples |
docs/src/pages/framed/{Component}/{Example}.svelte |
Interactive demos referenced by <FileSource> |
Routify picks up new .svx files automatically. docs/scripts/index-docs.ts indexes each page and its ## headings for search. No separate nav registration is required.
Prose conventions (backticks, plain language, SVX gotchas): see docs/COMPONENT_DOCS_STYLE.md.
Preview locally with cd docs && bun dev.
Before adding an example, read the component's .svx and follow its order, grouping, and voice. Reference pages: Button.svx, ComboBox.svx.
Page structure:
- Frontmatter block at the very top: a
description:field, pluscomponents:when the page renders an API table or imports components in a<script>. - Optional
<script>imports (components + icons used inline). ## Basicfirst, then one## Section titleper variant/feature, each with a short description then the demo.
Frontmatter description:
The lead sentence lives in the frontmatter description: field, not in body prose. It renders in the page hero.
- 1–2 plain sentences, ~180 characters max.
- Describe what the component is for, not how it works. No implementation details ("resize observer", "forwards mouse events", "keyed for performance").
- Renders as plain text: no inline
codeor[links]. - The body must start with a
##heading. A leading body paragraph is re-extracted by theheroIntroremark plugin insvelte.config.tsand silently overrides the frontmatterdescription. When lifting an old intro, fold any trailing sentences or links into the## Basicprose.
---
components: ["Dropdown", "DropdownSkeleton", "FluidDropdownSkeleton"]
description: Dropdowns provide a select input with a dropdown menu, with multiple states, sizes, and customization options.
---Headings and grouping:
- The first, plainest example is
## Basic— never## Default. - Keep headings concise. The page title already names the component, so drop the repeated component-noun prefix (
Header with app switcher→App switcher) and trailingstate/variant/size(Invalid state→Invalid,Light variant→Light,Small size→Small). - Group related examples under a
## Groupheading with###children when several share an axis:Sizes,States(invalid / warning / disabled / read-only),Fluid,Variants,Skeleton,Selectable,Filterable,Low contrast, and the like. Prefer this over a long flat list of##headings. Lead each group with its simplest member using a descriptive label (neverDefault); the group's base example can sit directly under the## Groupheading. SeeTextInput.svx(Sizes / States / Fluid),Tag.svx(Filterable / Selectable), andTreeView.svx. - Name variant families with a
Base (qualifier)form:Fluid (invalid),Icon-only (large).
Renaming headings (anchors):
Heading slugs are GitHub-style (rehype-slug): lowercase, spaces to -, parentheses and punctuation stripped (Fluid variant (invalid) → fluid-invalid). When you rename a heading that is a link target:
- Fix in-page links:
grep -nE '\(#' <file>.svx - Fix cross-page links:
grep -rn '<Component>#<old-anchor>' docs/src/pages - To keep an anchor while grouping, demote the heading to
###with its text unchanged — the slug is preserved. See#selectable/#radio-groupinContextMenu.svxandPortal#custom-target.
Description language: imperative, prop-focused. See Writing style for Carbon content rules.
- "Set
kind="secondary"for secondary actions." - "Hide the label visually by setting
hideLabeltotrue. The label remains available to screen readers."
Typical section order (follow sibling sections on that page):
## Basic- Core variants grouped (kinds, sizes)
- Feature sections (slots, filtering, typeahead, portal, etc.)
- Layout options (direction, light)
- Validation states (
invalid,warn) - Interaction states (
disabled,readonly) - Skeleton (if applicable)
- Advanced / performance (virtualization, async)
- Programmatic or reactive behavior last
Insert new sections in the logical group, and nest the variants above under ## Group + ### children (see Headings and grouping). Do not append every example to the end if it belongs with related variants. Reactive / bind:-driven examples belong high up near the basics; loading / skeleton examples go last.
Inline vs framed:
- Inline: simple, static demos directly in the
.svxfile <FileSource src="/framed/Component/Example" />: reactive state,bind:this, events, async behavior, or demos too large for inline. Add the Svelte file underdocs/src/pages/framed/{Component}/(PascalCase name, for exampleReactiveComboBox.svelte)
Use a framed example whenever the demo contains script logic (state, handlers, async work). The full source shows in the docs, so keep it readable:
- Do not add JSDoc to framed examples. They are usage demos, not the public API. Save typed JSDoc for the components under
src/. - Add short, concise, high-value code comments only where they earn their place, such as flagging that demo code is not production guidance:
// For demo purposes only: NEVER hardcode secrets in production.Skip comments that restate the code. - Simulate API or async behavior with
await new Promise((resolve) => setTimeout(resolve, 300))rather than wiring up a real network call. SeeCopyInputAsync.svelte.
Other patterns:
- No admonitions. Fold accessibility notes and non-obvious caveats into the relevant example's prose. Do not use
> [!NOTE]/> [!WARNING]/> [!TIP]/> [!CAUTION]blocks. - Keyboard keys in prose: use
<DocKbd label="Enter" />for named keys (Enter,Escape,Shift,Ctrl,Space, etc.). mdsvex auto-importsDocKbd; do not add a manual import. Do not use backticks (Escape), raw<kbd>, or bold (**Enter**). For combinations, use one component per key:<DocKbd label="⌘" />+<DocKbd label="C" />. Keep collective phrases as plain text ("arrow keys", "modifier keys", "keyboard navigation"). Prop values and UI copy inside component examples are out of scope (e.g.placeholder="Enter user name...",shortcutText="⌘C"). - Cross-links to related components:
[Modal](/components/Modal#modal-with-dropdowns) - Reuse the same sample data shapes as neighboring examples on that page
- For layout and spacing (flex,
gap, stacked elements), use theStackcomponent rather than raw HTML with ad-hoc inline styles. Reserve hand-written layout markup for casesStackcannot express. - Add
data-outlineto an element when an outline makes the demo clearer, for example marking a right-click target in a context menu. The framed module provides the global style; see<p data-outline …>inContextMenuTarget.svelte.
Follow Carbon's writing style: direct, concise, present tense. Section descriptions explain what a prop does and when to use it, not what the component "allows."
Inline code in .svx prose is styled as Carbon code tokens. Keep backticks sparse so section intros stay readable.
Split of responsibilities:
- JSDoc + auto-generated API tables = exhaustive identifier reference
.svxprose = readable explanation; examples = copy-paste usage
Backtick when essential (one primary target per paragraph):
- The prop/API being configured in that section
- Literal values the reader copies (
true,"compact") - Methods/utilities (
TreeView.expandAll()) - Svelte/HTML tokens (
let:node,bind:checked,role="treeitem")
Use plain language instead:
- Data shape in words ("unique id and display text") rather than listing
id,text,disabledin one sentence - Drop filler: "the prop", "the method" (prefer "Set
activeId" not "Set theactiveIdprop") - Cross-link sibling sections by name instead of inline prop lists
Do not edit in prose-only passes:
- Live Svelte examples,
<FileSource />, fenced code blocks, reference tables (events/gestures/slot vars)
In .svx files, markdown prose is compiled as Svelte. Bare { ... } in prose is parsed as JavaScript, not displayed text.
| Bad (runtime error) | Good |
|---|---|
`event.detail` as { key, direction } |
`event.detail` shaped like `{ key, direction }` |
receives (a, b, { key, ascending }) |
receives `(a, b, { key, ascending })` |
Always wrap object literals and expressions in backticks, or rephrase without braces. See DataTable.svx sort section for a real incident.
| Avoid | Prefer |
|---|---|
| Trivializers: "easy", "easily", "makes it easier to", "for performance reasons" | State the behavior directly |
Latin abbreviations: e.g., i.e. |
"for example", "that is" (or rephrase without) |
| Indirect phrasing: "allows you to", "allowing you to", "allows for" | Direct imperative: "Set … to …", "Use … to …" |
| Filler tails that restate the obvious: "This provides more visual emphasis.", "This prevents user interaction." | Cut unless it adds when-to-use guidance |
| Passive or future tense in intros | Present tense, active voice |
Varied hideLabel / hideLegend wording |
Standard boilerplate (below) |
| Listing every prop in backticks in one intro sentence | Plain-language data shape; one configuring prop backtick per paragraph |
`selectedIds` tracks a single id` (implementation narration) |
"Only one node is selected at a time" |
| Prop laundry lists in intros | Links to sibling sections (TreeView Basic pattern) |
Standard boilerplate for hidden labels (copy verbatim):
Hide the label visually by setting
hideLabeltotrue. The label remains available to screen readers.
Use hideLegend analogously for legend-hiding props.
Before / after examples:
# Before
Create a basic tree view using the `nodes` prop. Each node requires a unique `id` and `text`, with optional properties for `disabled`, `icon`, and child `nodes`.
# After
Create a basic tree view using the `nodes` prop. Each node needs a unique id and display text; icons, disabled state, and nested children are optional.
# Before
Set the initial active node using the `activeId` prop.
# After
Set the initial active node using `activeId`.
# Before
Set openOnClear to true to allow users to immediately see all available items.
# After
Set `openOnClear` to `true` to reopen the dropdown menu after clearing the selection.
This lets users immediately browse all available items without clicking the input again.
# Before
For async (e.g., server-side) filtering, bind to value…
# After
For async (for example, server-side) filtering, bind to `value`…
# Before
Virtualization allows you to render large lists efficiently for performance reasons.
# After
Virtualization renders only the items currently visible in the viewport, improving performance for large lists.
# Before
The menu closes from the trigger, the `Escape` key, or an outside click.
# After
The menu closes from the trigger, <DocKbd label="Escape" />, or an outside click.
- Export from
src/index.js - JSDoc all public API →
bun build:docs - Create
docs/src/pages/components/{Component}.svxmodeled on a similar existing component: a frontmatterdescription:, then## Basicfirst (see Example conventions for structure and grouping). Follow prose conventions inCOMPONENT_DOCS_STYLE.md. - Add framed examples only where interactivity requires them
- Preview with
cd docs && bun dev
The library pins carbon-components to v10 (10.58.x). When a style fix or feature from newer Carbon is missing from the v10 SCSS, or when the Svelte components need a tweak Carbon does not provide, put the patch in a hand-authored SCSS partial under css/. These compile into the shipped theme stylesheets. Use them instead of per-component <style> blocks for anything that should be themeable.
Each patch is a leading-underscore partial (for example css/_breadcrumb.scss, css/_tag.scss) that imports the Carbon variables and mixins it needs, defines a single named mixin, and emits it through Carbon's exports() import-once guard:
@import "carbon-components/scss/globals/scss/vars";
@import "carbon-components/scss/globals/scss/typography";
/// Small breadcrumb variant (Carbon React `size="sm"` parity)
/// @access private
/// @group components
@mixin breadcrumb-sm {
.#{$prefix}--breadcrumb--sm {
@include type-style("label-01");
margin-right: $carbon--spacing-02;
}
}
@include exports("breadcrumb-sm") {
@include breadcrumb-sm;
}Style only through Carbon tokens and mixins: spacing ($carbon--spacing-*), type (type-style(...)), to-rem(...), and theme color tokens. Do not hardcode raw px or hex values. Tokens keep themes consistent and survive a future Carbon upgrade.
Reference classes through $prefix (.#{$prefix}--breadcrumb), never a literal .bx--….
Do not use :has(). It exceeds the Svelte 5 browserslist baseline (Firefox 83/Safari 14) the CSS targets, and Lightning CSS cannot prefix or polyfill it. Mark parents explicitly instead (for example a hasLeftIcon prop emitting a marker class). Same rule for any selector newer than that baseline.
Wrap output in @include exports("name") so a partial imported by multiple theme entry files emits its rules only once.
Document the mixin with SassDoc (/// @access private, /// @group components).
Token utility partials (css/_type.scss, css/_box.scss, …) emit bx--type-* and bx--box-* classes. Map v11 token names in docs and props to v10 theme variables in SCSS where needed (for example layer-01 → $ui-01). Share spacing scale values through css/_spacing-scale.scss when multiple partials use the same rem steps.
A partial ships only after a theme entry file imports it. Add @import "./name"; to the custom component overrides block, which comes after Carbon's own globals/scss/styles, in all six entry files. Keep import order identical across them:
css/all.scsscss/white.scsscss/g10.scsscss/g80.scsscss/g90.scsscss/g100.scss
After editing any .scss under css/, you must run bun build:css locally:
bun build:cssThe compiled css/*.css are not committed (they are gitignored) — but they are still required at runtime, so without this step your local docs site and e2e tests will use stale (or missing) styles. bun setup runs build:css once on a fresh clone, and CI/release run it on demand; neither helps an in-progress local edit.
This compiles every non-partial *.scss (sass, compressed) through Lightning CSS and writes css/*.css plus css/css.d.ts. Commit only the .scss source — never the generated *.css. The small css/css.d.ts (module declarations) is committed, so type-checks resolve the CSS imports without a build; commit it too if adding or removing a theme entry changes it.
The prebundled CSS targets the Svelte 5 browser support baseline (Firefox 83/Safari 14), not older browsers. Matching that baseline lets Lightning CSS drop legacy fallbacks. Author SCSS to that baseline; see the :has() note in Conventions.
See Best practices for scoping. Run checks against what you changed. Full-repo runs are slow and surface unrelated failures.
For format and lint, scope Biome to what you touched:
# Specific paths — always lints the current working tree
bunx biome check --write src/DataTable
# Staged files only (what a commit would include)
bunx biome check --write --staged
# Files committed on this branch relative to master
bunx biome check --write --changed --since=masterPick the form that matches your state. --changed and --staged resolve their file
list from git, so each sees a different slice:
- Path scoping (
src/DataTable) lints whatever is on disk now, committed or not. Use it while editing. --stagedlints the git index. Run it aftergit add.--changeddiffs commits against the default branch. It does not see uncommitted or unstaged edits, so it reports nothing until you commit.
bun run lint runs Biome over the entire repository. Reserve it for broad sweeps.
For unit tests, match a pattern against test file paths instead of running the whole suite:
bun run test DataTableThe full suite (bun run test) is slow and can hit unrelated flaky UIShell focus failures. Scope to the component you touched.
Types and E2E:
bun test:src-typestype-checkssrc/(usestsconfig.types.json)bun test:typesrunssvelte-checkon*.svelteand.tsfiles intests/bunx playwright test --grep "Breakpoint"runs a focused E2E pattern (see E2E testing);bun run test:e2eruns the full E2E suite
Unit tests live in tests/ and use Vitest with @testing-library/svelte. See Checks for scoped runs.
tests/
ComponentName/
ComponentName.test.ts # assertions
ComponentName.test.svelte # default fixture
ComponentName.custom.test.svelte # variant fixtures as needed
Mirror src/ component folders. Put complex setups in *.test.svelte, not inline in the .ts file.
In .test.svelte fixtures, import the component under test directly, not from the barrel:
<script lang="ts">
import ComboBox from "carbon-components-svelte/ComboBox/ComboBox.svelte";
</script>Avoid import { ComboBox } from "carbon-components-svelte" in fixtures. Direct paths skip transforming the entire src/index.js barrel and are faster under Vitest. The alias in vite.config.ts resolves carbon-components-svelte to src/.
In .test.ts files, import types from the direct path and render via the fixture:
import type ComboBoxComponent from "carbon-components-svelte/ComboBox/ComboBox.svelte";
import ComboBox from "./ComboBox.test.svelte";Import shared utilities (for example exported helpers) from the component file or src/utils/ as appropriate.
- Prefer accessible queries:
getByRole,getByLabelText,getByTextwithexact: truewhen needed (ComboBox.test.ts). - Use
data-testidin unit tests only when roles are insufficient. E2E fixtures usedata-testidroutinely. - Use the shared
userhelper fromtests/utils/user.tsfor clicks and keyboard input. - Type fixture props with
ComponentProps<typeof Component>fromsveltewhere helpful.
Prioritize high-value coverage:
- Default render and primary user interactions (open, select, submit, keyboard)
- Accessibility roles, labels, and ARIA state changes
- Regressions for bugs you fix
- Boundary inputs to pure utils: zero or negative sizes, empty collections, divide-by-zero, and out-of-range indices. Assert the defined fallback (clamp, empty, identity) rather than only that it doesn't throw.
- Generic/type contracts when adding
@templateprops (see below)
Skip or avoid:
- Tests that mirror implementation details without asserting user-visible behavior
- Redundant permutations of the same code path
- Large fixture setups when a focused unit test on a util suffices
Add tests proportional to the change. Not every prop variant needs its own case.
Vitest exposes expectTypeOf globally (see tests/utils/setup-globals.ts). Use it in a describe("Generics", …) block to verify @template JSDoc flows through to consumer types. Pure type assertions do not require a runtime render.
Pattern from Button.test.ts and ComboBox.test.ts:
import type ComboBoxComponent from "carbon-components-svelte/ComboBox/ComboBox.svelte";
import type { ComponentProps } from "svelte";
describe("Generics", () => {
it("should support custom item types with generics", () => {
type Product = { id: string; text: string; price: number };
type ComponentType = ComboBoxComponent<Product>;
type Props = ComponentProps<ComponentType>;
expectTypeOf<Props["items"]>().toEqualTypeOf<readonly Product[]>();
const itemToString = (item: Product) => item.text;
expectTypeOf(itemToString).parameter(0).toEqualTypeOf<Product>();
});
});Use ComponentProps and ComponentEvents from svelte for props and event payloads. For runtime smoke tests with custom item shapes, add a *Generics.test.svelte fixture (ComboBoxGenerics.test.svelte).
Common expectTypeOf matchers: .toEqualTypeOf, .toExtend, .parameter(n), .returns, .toHaveProperty.
beforeEach(() => vi.clearAllMocks())when tests use spies.- Scope runs:
bun run test ComboBox(see Checks).
The default bun run test harness uses Svelte 5. Separate workspaces under tests-svelte3/ and tests-svelte4/ run the same tests against older Svelte versions.
You only need these when fixing a failure reported from bun run test:svelte3, bun run test:svelte4, or their type-check scripts. Most changes do not require them.
Before running a compatibility script, install that workspace's dependencies. Without a local node_modules, the run may fall back to the Svelte 5 harness:
cd tests-svelte3 && bun install
cd ../tests-svelte4 && bun installThen from the repo root:
bun run test:svelte3
bun run test:svelte4E2E tests run in a real browser against HTML fixtures served by Vite. The Playwright config is in playwright.config.ts; the Vite config for fixtures is in e2e/vite.config.ts.
Run the full suite:
bun run test:e2eRun a focused component or pattern:
# Single test file
bunx playwright test e2e/breakpoint.test.ts
# Tests matching a grep pattern (for example "Breakpoint" or "sm breakpoint")
bunx playwright test --grep "Breakpoint"The fixtures are a Vite multi-page app, one .html entry per fixture. How they are served depends on the environment, controlled by the CI env var in playwright.config.ts:
- Locally: Playwright starts the Vite dev server. It transforms modules on demand, so there is no build step and edits show up immediately.
- In CI: Playwright runs
vite buildonce and serves the static output withvite preview. Bundled assets load faster and with less run-to-run variance than the dev server, which transforms unbundled modules on demand through a single process that competes with the browser workers for CPU.
Neither path needs a manual build. To reproduce the CI build locally, for example to debug a build-only failure, build the fixtures once:
bunx vite build --config e2e/vite.config.tsThe output lands in e2e/fixtures/dist/ (gitignored). Serve it with bunx vite preview --config e2e/vite.config.ts, or run the whole CI path in one command:
CI=true bunx playwright test --project=chromiumTo add a new E2E test, copy the Breakpoint example. Create these files:
| File | Purpose |
|---|---|
e2e/fixtures/MyComponentFixture.svelte |
Svelte component that renders the component under test. Use data-testid on elements you want to query. |
e2e/fixtures/my-component.ts |
Entry script that mounts the fixture into #app. Use the shared mount() utility from ./mount. |
e2e/fixtures/my-component.html |
HTML page with <div id="app"></div> and a script tag loading the entry module. |
e2e/my-component.test.ts |
Playwright tests. Use page.goto("/my-component.html") in beforeEach, then page.getByTestId(...) to assert. |
Example fixture mount (my-component.ts):
import MyComponentFixture from "./MyComponentFixture.svelte";
import { mount } from "./mount";
mount(MyComponentFixture);A few gotchas:
For components that hide content with CSS (for example ComposedModal), toBeVisible() can lie because the element stays in the DOM. Prefer toHaveClass(/is-visible/) on the container, or assert on aria-hidden / inert.
getByText("Row 1") also matches "Row 10", "Row 11", and so on. Use getByRole("cell", { name: "Row 1", exact: true }) or a more specific selector when content can overlap.
Add a link to e2e/fixtures/index.html so the fixture is reachable from the index page.
Use Conventional Commits:
<type>(<scope>): <subject>
[optional body]
Subject line:
- Keep it concise: one line, imperative mood
- Common types:
fix,feat,docs,chore,test,refactor - Scope is the component or area; multi-word names are lowercase with dashes:
combo-box,code-snippet,data-table,ui-shell,accordion-item - Omit scope when the change spans many areas (
docs: …,chore: …) - Append
!after the scope for breaking changes:fix(accordion-item)!: …
Examples:
fix(combo-box): close dropdown on outside click
docs(tree-view): fix slottable inline editing example
chore(deps-dev): bump vitest, svelte-check
Body (optional):
A body is not required. Add one when context helps reviewers or when closing an issue.
- Reference the issue:
Fixes #1000orCloses #1000(GitHub auto-closes on merge) - At most 2-3 sentences, full sentences, no bullet lists
- Wrap lines at 72 characters for readability in
git log
Example with body:
fix(data-table): associate cells with column headers
Cells now set aria-labelledby to their column header id.
Fixes #3162
Avoid vague subjects (fix bug), PascalCase or camelCase scopes (fix(ComboBox):), and long subjects. Move detail to the body. Prefer Fixes #N in the body over PR numbers in the subject.
Follow Commit messages for each commit in your branch.
Before you open a PR, sync your fork with upstream:
git fetch upstream
git checkout master
git merge upstream/masterPush your branch, then open a PR comparing <YOUR_USER_ID>/feature to origin/master.
The following applies only to project maintainers.
This library publishes to NPM with provenance via a GitHub workflow.
Pushing a tag that starts with v (for example v0.81.1) triggers the workflow. It runs bun ci, bun build:docs, and bunx culls --preserve=svelte before publishing to NPM.
Maintainers still do a few things locally before tagging.
On a clean master branch, run bun run release. That will:
- Bump the semantic version in
package.json - Generate notes in
CHANGELOG.md - Run
bun run build:docsto update generated documentation
It does not commit or tag. Do that manually:
# 1. Commit the changes using the new version as the commit message.
git commit -am "v0.81.1"
# 2. Create a tag.
git tag v0.81.1
# 3. Push the tag to the remote.
# This triggers the `release.yml` workflow to publish a new package to NPM (with provenance).
git push origin v0.81.1If the workflow succeeds, the release.yml workflow publishes the new version to NPM.
After the release is on NPM:
-
Create a new release on GitHub. Click "Generate release notes" to list changes by commit with PR and author metadata. Drop notes that do not matter for the release (for example CI-only changes).
-
Mark it as the latest release.
-
On each related PR or issue, confirm the fix shipped. Future readers will want to know which version picked it up.
Released in [v0.81.1](https://github.com/carbon-design-system/carbon-components-svelte/releases/tag/v0.81.1).