Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ packages/*/build/
packages/*/examples/*/build/
es
es-custom
scoped-elements
lib
dist
umd
Expand Down
169 changes: 169 additions & 0 deletions docs/guides/scoped-elements.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
# Scoped custom elements

## Overview

The `scoped-elements` build output provides a version of Carbon web components
that use
[scoped custom element registries](https://github.com/WICG/webcomponents/blob/gh-pages/proposals/Scoped-Custom-Element-Registries.md)
instead of the global custom element registry. This allows multiple versions of
Carbon web components to coexist on the same page without naming conflicts.

## When to use scoped elements

Use the `scoped-elements` build output when:

- **Multiple versions**: You need to use different versions of Carbon web
components on the same page (e.g., during a gradual migration)
- **Micro-frontends**: Your application uses a micro-frontend architecture where
different teams may use different versions of Carbon
- **Third-party integration**: You're integrating with third-party code that may
also use Carbon web components
- **Avoiding conflicts**: You want to prevent custom element naming conflicts in
complex applications

## How it works

### Standard build (`es/`)

The standard build uses the `@customElement` decorator to register components
globally:

```typescript
@customElement('cds-button')
class CDSButton extends LitElement {
// Component implementation
}
```

This registers the component in the global `customElements` registry, which
means only one version of `cds-button` can exist on a page.

### Scoped elements build (`scoped-elements/`)

The `scoped-elements` build transforms components to:

1. **Remove global registration**: The `@customElement` decorator is stripped
out, so components are not automatically registered globally
2. **Add scoped registries**: Each component that uses child components gets its
own private custom element registry
3. **Isolate dependencies**: Child components are registered only within their
parent's shadow DOM scope

This transformation is performed by the `scopedElementsDecoratorStripPlugin`
during the build process.

## Installation

The `scoped-elements` build is included in the `@carbon/web-components` package:

```bash
npm install @carbon/web-components
```

## Usage

### Basic usage

Import components from the `scoped-elements` directory instead of `es`:

```javascript
// Instead of:
// import '@carbon/web-components/es/components/button/button.js';

// Use:
import CDSButton from '@carbon/web-components/scoped-elements/components/button/button.js';
```

### Scoped subcomponents

For components with child dependencies, the scoped build automatically creates
private registries:

```javascript
import CDSDropdown from '@carbon/web-components/scoped-elements/components/dropdown/dropdown.js';

// The dropdown component will automatically register its child components
// (like dropdown-item) in its own scoped registry
customElements.define('cds-dropdown', CDSDropdown);
```

## Complete example with the accordion component

```html
<div data-scoped-accordion-root>
<cds-accordion-skeleton count="3" open></cds-accordion-skeleton>
<cds-accordion>
<cds-accordion-item title="Section 1">
<p>Rendered from a scoped registry.</p>
</cds-accordion-item>
<cds-accordion-item title="Section 2">
<p>Not defined in the global registry.</p>
</cds-accordion-item>
</cds-accordion>
</div>
```

```js
import CDSAccordion from '@carbon/web-components/scoped-elements/components/accordion/accordion.js';
import CDSAccordionItem from '@carbon/web-components/scoped-elements/components/accordion/accordion-item.js';
import CDSAccordionSkeleton from '@carbon/web-components/scoped-elements/components/accordion/accordion-skeleton.js';

/**
*
* When using the scoped-elements build, components are not automatically
* registered globally. This array contains the accordion component family
* (accordion, accordion-item, and accordion-skeleton) with their tag names
* and class constructors for manual registration.
*
*/
const elements = [
['cds-accordion', CDSAccordion],
['cds-accordion-item', CDSAccordionItem],
['cds-accordion-skeleton', CDSAccordionSkeleton],
];

const defineElements = (registry) => {
elements.forEach(([tagName, elementClass]) => {
if (!registry.get(tagName)) {
registry.define(tagName, elementClass);
}
});
};

const root = document.querySelector('[data-scoped-accordion-root]');

/**
*
* Wraps the root element in a shadow DOM with a scoped custom element registry.
*
*/
if (root) {
const host = document.createElement('div');
const markup = root.outerHTML;
const fallbackRoot = root;

root.replaceWith(host);

try {
const registry = new CustomElementRegistry();
defineElements(registry);

// Attach shadow DOM with the scoped registry
// Components inside this shadow root will use the scoped registry
// instead of the global customElements registry.
const shadowRoot = host.attachShadow({
mode: 'open',
customElementRegistry: registry,
});

shadowRoot.innerHTML = markup;

if (typeof registry.upgrade === 'function') {
registry.upgrade(shadowRoot);
}
} catch (error) {
host.replaceWith(fallbackRoot);
throw error;
}
}
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# See https://help.github.com/ignore-files/ for more about ignoring files.

# dependencies
/node_modules

# testing
/coverage

# production
/build

# misc
.DS_Store
.cache
.env.local
.env.development.local
.env.test.local
.env.production.local

npm-debug.log*
yarn-debug.log*
yarn-error.log*
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"includePaths": [
"node_modules",
"../../node_modules"
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
<!--
Copyright IBM Corp. 2026

This source code is licensed under the Apache-2.0 license found in the
LICENSE file in the root directory of this source tree.
-->

<html>
<head>
<title>carbon-web-components scoped custom registry example</title>
<meta charset="UTF-8" />
<link rel="stylesheet"
href="https://1.www.s81c.com/common/carbon-for-ibm-dotcom/tag/v1/latest/plex.css"/>

<link rel="stylesheet" href="src/styles.scss" />
<style>
/* Suppress custom element until styles are loaded */
cds-accordion:not(:defined) {
display: none;
}

body {
font-family: 'IBM Plex Sans', Arial, sans-serif;
margin: 2rem;
}

section + section {
margin-top: 2rem;
}
</style>
<script type="module" src="src/index.js"></script>
</head>
<body>
<section>
<div data-scoped-accordion-root>
<cds-accordion-skeleton count="3" open></cds-accordion-skeleton>
<cds-accordion>
<cds-accordion-item title="Scoped section 1 title">
<p>
This accordion is created with a scoped CustomElementRegistry from
src/index.js.
</p>
</cds-accordion-item>
<cds-accordion-item title="Scoped section 2 title">
<p>
It does not rely on the global customElements registry.
</p>
</cds-accordion-item>
</cds-accordion>
</div>
</section>
</body>
</html>
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
"name": "carbon-web-components-scoped-registry-example",
"version": "0.1.0",
"private": true,
"description": "Sample project for getting started with the Web Components from Carbon using a scoped registry.",
"license": "Apache-2",
"main": "index.html",
"scripts": {
"build": "vite build",
"clean": "rimraf node_modules dist .cache",
"dev": "vite"
},
"dependencies": {
"@carbon/styles": "^1.34.0",
"@carbon/web-components": "latest",
"sass": "^1.64.1"
},
"devDependencies": {
"vite": "^7.1.2",
"rimraf": "^6.0.1"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"template": "node"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
/**
* Copyright IBM Corp. 2026
*
* This source code is licensed under the Apache-2.0 license found in the
* LICENSE file in the root directory of this source tree.
*/
import CDSAccordion from '@carbon/web-components/scoped-elements/components/accordion/accordion.js';
import CDSAccordionItem from '@carbon/web-components/scoped-elements/components/accordion/accordion-item.js';
import CDSAccordionSkeleton from '@carbon/web-components/scoped-elements/components/accordion/accordion-skeleton.js';

const accordionElements = [
['cds-accordion', CDSAccordion],
['cds-accordion-item', CDSAccordionItem],
['cds-accordion-skeleton', CDSAccordionSkeleton],
];

const defineAccordionElements = (registry) => {
accordionElements.forEach(([tagName, elementClass]) => {
if (!registry.get(tagName)) {
registry.define(tagName, elementClass);
}
});
};

const createScopedRegistry = () => {
if (typeof CustomElementRegistry === 'undefined') {
return undefined;
}

try {
return new CustomElementRegistry();
} catch {
return undefined;
}
};

const scopedAccordionRoot = document.querySelector('[data-scoped-accordion-root]');

if (scopedAccordionRoot) {
const host = document.createElement('div');
const markup = scopedAccordionRoot.outerHTML;
const fallbackRoot = scopedAccordionRoot;

scopedAccordionRoot.replaceWith(host);

try {
const scopedRegistry = createScopedRegistry();
if (!scopedRegistry) {
host.replaceWith(fallbackRoot);
Comment on lines +48 to +49

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This fallback restores the cds-accordion* code but doesn't keep a scoped registry attached or register those tags with customElements.define(). Will browsers without CustomElementRegistry support (support appears to be incomplete: https://developer.mozilla.org/en-US/docs/Web/API/CustomElementRegistry#browser_compatibility) leave the accordion elements unupgraded here?

Unless I'm misunderstanding, MDN says custom elements have to be registered to be available: https://developer.mozilla.org/en-US/docs/Web/API/Web_components/Using_custom_elements#registering_a_custom_element

} else {
defineAccordionElements(scopedRegistry);

const shadowRoot = host.attachShadow({
mode: 'open',
customElementRegistry: scopedRegistry,
});

shadowRoot.innerHTML = markup;
if (typeof scopedRegistry.upgrade === 'function') {
scopedRegistry.upgrade(shadowRoot);
}

if (!shadowRoot.querySelector('cds-accordion')) {
throw new Error(
'Accordion markup was not rendered in scoped shadow root.'
);
}
}
} catch (error) {
host.replaceWith(fallbackRoot);
const message =
error instanceof Error ? error.message : 'Unknown scoped registry error.';
throw new Error(`Scoped registry initialization failed: ${message}`);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
@use '@carbon/styles/scss/reset';
@use '@carbon/styles/scss/theme';
@use '@carbon/styles/scss/themes';

:root {
@include theme.theme(themes.$white);
background-color: var(--cds-background);
color: var(--cds-text-primary);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { resolve } from 'path';
import { defineConfig } from 'vite';

export default defineConfig({
build: {
rollupOptions: {
input: {
main: resolve(__dirname, 'index.html'),
},
},
},
});
Loading
Loading