diff --git a/docs/_sidebar.md b/docs/_sidebar.md
index 018064afc..73aff9306 100644
--- a/docs/_sidebar.md
+++ b/docs/_sidebar.md
@@ -59,6 +59,7 @@
- [Format Date](/components/format-date)
- [Format Number](/components/format-number)
- [Include](/components/include)
+ - [Mutation Observer](/components/mutation-observer)
- [Relative Time](/components/relative-time)
- [Resize Observer](/components/resize-observer)
- [Responsive Media](/components/responsive-media)
diff --git a/docs/components/mutation-observer.md b/docs/components/mutation-observer.md
new file mode 100644
index 000000000..1eddefcb9
--- /dev/null
+++ b/docs/components/mutation-observer.md
@@ -0,0 +1,104 @@
+# Mutation Observer
+
+[component-header:sl-mutation-observer]
+
+The Mutation Observer component offers a thin, declarative interface to the [`MutationObserver API`](https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver).
+
+The mutation observer will report changes to the content it wraps through the `sl-mutation` event. When emitted, a collection of [MutationRecord](https://developer.mozilla.org/en-US/docs/Web/API/MutationRecord) objects will be attached to `event.detail` that contains information about how it changed.
+
+```html preview
+
+
+ Click to mutate
+
+
+
+ 👆 Click the button and watch the console
+
+
+
+
+
+```
+
+?> When you create a mutation observer, you must indicate what changes it should respond to by including at least one of `attr`, `child-list`, or `char-data`. If you don't specify at least one of these attributes, no mutation events will be emitted.
+
+## Examples
+
+### Child List
+
+Use the `child-list` attribute to watch for new child elements that are added or removed.
+
+```html preview
+
+
+
+ Add button
+
+
+
+ 👆 Add and remove buttons and watch the console
+
+
+
+
+
+```
+
+[component-metadata:sl-mutation-observer]
diff --git a/docs/resources/changelog.md b/docs/resources/changelog.md
index 639fa2662..46f41dc2b 100644
--- a/docs/resources/changelog.md
+++ b/docs/resources/changelog.md
@@ -11,6 +11,7 @@ _During the beta period, these restrictions may be relaxed in the event of a mis
- 🚨 BREAKING: removed `` (use `` instead)
- 🚨 BREAKING: removed `percentage` attribute from `` and `` (use `value`) instead
- 🚨 BREAKING: switched the default `type` of `` from `primary` to `neutral`
+- Added the experimental `` component
- Added the `` component
- Added `--sl-surface-base` and `--sl-surface-base-alt` as early surface tokens to improve the appearance of alert, card, and panels in dark mode
- Added the `--sl-panel-border-width` design token
diff --git a/src/components/mutation-observer/mutation-observer.styles.ts b/src/components/mutation-observer/mutation-observer.styles.ts
new file mode 100644
index 000000000..cb10fcc60
--- /dev/null
+++ b/src/components/mutation-observer/mutation-observer.styles.ts
@@ -0,0 +1,10 @@
+import { css } from 'lit';
+import componentStyles from '../../styles/component.styles';
+
+export default css`
+ ${componentStyles}
+
+ :host {
+ display: contents;
+ }
+`;
diff --git a/src/components/mutation-observer/mutation-observer.test.ts b/src/components/mutation-observer/mutation-observer.test.ts
new file mode 100644
index 000000000..6285c3b79
--- /dev/null
+++ b/src/components/mutation-observer/mutation-observer.test.ts
@@ -0,0 +1,13 @@
+import { expect, fixture, html, waitUntil } from '@open-wc/testing';
+// import sinon from 'sinon';
+
+import '../../../dist/shoelace.js';
+import type SlMutationObserver from './mutation-observer';
+
+describe('', () => {
+ it('should render a component', async () => {
+ const el = await fixture(html` `);
+
+ expect(el).to.exist;
+ });
+});
diff --git a/src/components/mutation-observer/mutation-observer.ts b/src/components/mutation-observer/mutation-observer.ts
new file mode 100644
index 000000000..1ac6b9f49
--- /dev/null
+++ b/src/components/mutation-observer/mutation-observer.ts
@@ -0,0 +1,112 @@
+import { LitElement, html } from 'lit';
+import { customElement, property } from 'lit/decorators.js';
+import { emit } from '../../internal/event';
+import { watch } from '../../internal/watch';
+import styles from './mutation-observer.styles';
+
+/**
+ * @since 2.0
+ * @status experimental
+ *
+ * @event sl-mutation - Emitted when a mutation occurs.
+ *
+ * @slot - The content to watch for mutations.
+ */
+@customElement('sl-mutation-observer')
+export default class SlMutationObserver extends LitElement {
+ static styles = styles;
+
+ private mutationObserver: MutationObserver;
+
+ /**
+ * Watches for changes to attributes. If empty, all changes will be reported. To watch only specific attributes,
+ * separate them by a space.
+ */
+ @property({ reflect: true }) attr: string;
+
+ /** Indicates whether or not the attribute's previous value should be recorded when monitoring changes. */
+ @property({ attribute: 'attr-old-value', type: Boolean, reflect: true }) attrOldValue = false;
+
+ /** Watches for changes to the character data contained within the node. */
+ @property({ attribute: 'char-data', type: Boolean, reflect: true }) charData = false;
+
+ /** Indicates whether or not the previous value of the node's text should be recorded. */
+ @property({ attribute: 'char-data-old-value', type: Boolean, reflect: true }) charDataOldValue = false;
+
+ /** Watches for the addition or removal of new child nodes. */
+ @property({ attribute: 'child-list', type: Boolean, reflect: true }) childList = false;
+
+ /** Disables the observer. */
+ @property({ type: Boolean, reflect: true }) disabled = false;
+
+ connectedCallback() {
+ super.connectedCallback();
+ this.handleMutation = this.handleMutation.bind(this);
+
+ this.mutationObserver = new MutationObserver(this.handleMutation);
+ this.startObserver();
+ }
+
+ disconnectedCallback() {
+ this.stopObserver();
+ }
+
+ @watch('disabled')
+ handleDisabledChange() {
+ if (this.disabled) {
+ this.stopObserver();
+ } else {
+ this.startObserver();
+ }
+ }
+
+ @watch('attr', { waitUntilFirstUpdate: true })
+ @watch('attr-old-value', { waitUntilFirstUpdate: true })
+ @watch('char-data', { waitUntilFirstUpdate: true })
+ @watch('char-data-old-value', { waitUntilFirstUpdate: true })
+ @watch('childList', { waitUntilFirstUpdate: true })
+ handleChange() {
+ this.stopObserver();
+ this.startObserver();
+ }
+
+ handleMutation(mutationList: MutationRecord[]) {
+ emit(this, 'sl-mutation', {
+ detail: { mutationList }
+ });
+ }
+
+ startObserver() {
+ try {
+ this.mutationObserver.observe(this, {
+ subtree: true,
+ childList: this.childList,
+ attributes: typeof this.attr === 'string',
+ attributeFilter: typeof this.attr === 'string' && this.attr.length > 0 ? this.attr.split(' ') : undefined,
+ attributeOldValue: this.attrOldValue,
+ characterData: this.charData,
+ characterDataOldValue: this.charDataOldValue
+ });
+ } catch {
+ //
+ // A mutation observer was created without one of the required attributes: attr, char-data, or child-list. The
+ // browser will normally throw an error, but we'll suppress that so it doesn't appear as attributes are added
+ // and removed.
+ //
+ }
+ }
+
+ stopObserver() {
+ this.mutationObserver.disconnect();
+ }
+
+ render() {
+ return html` `;
+ }
+}
+
+declare global {
+ interface HTMLElementTagNameMap {
+ 'sl-mutation-observer': SlMutationObserver;
+ }
+}
diff --git a/src/shoelace.ts b/src/shoelace.ts
index e58bdfbae..0ae166038 100644
--- a/src/shoelace.ts
+++ b/src/shoelace.ts
@@ -27,6 +27,7 @@ export { default as SlInput } from './components/input/input';
export { default as SlMenu } from './components/menu/menu';
export { default as SlMenuItem } from './components/menu-item/menu-item';
export { default as SlMenuLabel } from './components/menu-label/menu-label';
+export { default as SlMutationObserver } from './components/mutation-observer/mutation-observer';
export { default as SlProgressBar } from './components/progress-bar/progress-bar';
export { default as SlProgressRing } from './components/progress-ring/progress-ring';
export { default as SlQrCode } from './components/qr-code/qr-code';