diff --git a/packages/webawesome/docs/_includes/sidebar.njk b/packages/webawesome/docs/_includes/sidebar.njk
index 18ed29c1f..c747b9951 100644
--- a/packages/webawesome/docs/_includes/sidebar.njk
+++ b/packages/webawesome/docs/_includes/sidebar.njk
@@ -98,6 +98,7 @@
Icon
Include
Input
+ Intersection Observer
Mutation Observer
Popover
Popup
diff --git a/packages/webawesome/docs/docs/components/intersection-observer.md b/packages/webawesome/docs/docs/components/intersection-observer.md
new file mode 100644
index 000000000..1252b2bcd
--- /dev/null
+++ b/packages/webawesome/docs/docs/components/intersection-observer.md
@@ -0,0 +1,297 @@
+---
+title: Intersection Observer
+description: Tracks immediate child elements and fires events as they move in and out of view.
+layout: component
+---
+
+This component leverages the [IntersectionObserver API](https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserver) to track when its direct children enter or leave a designated root element. The `wa-intersect` event fires whenever elements cross the visibility threshold.
+
+```html {.example}
+
+
+Scroll to see the element intersect at 100% visibility
+
+
+```
+
+:::info
+Keep in mind that only direct children of the host element are monitored. Nested elements won't trigger intersection events.
+:::
+
+## Usage Examples
+
+### Adding Observable Content
+
+The intersection observer tracks only its direct children. The component uses [`display: contents`](https://developer.mozilla.org/en-US/docs/Web/CSS/display#contents) styling, which makes it seamless to integrate with flex and grid layouts from a parent container.
+
+```html
+
+
+ Box 1
+ Box 2
+ Box 3
+
+
+```
+
+The component tracks elements as they enter and exit the root element (viewport by default) and emits the `wa-intersect` event on state changes. The event provides `event.detail.entry`, an [`IntersectionObserverEntry`](https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserverEntry) object with intersection details.
+
+You can identify the triggering element through `entry.target`. Check `entry.isIntersecting` to determine if an element is entering or exiting the viewport.
+
+```javascript
+observer.addEventListener('wa-intersect', event => {
+ const entry = event.detail.entry;
+
+ if (entry.isIntersecting) {
+ console.log('Element entered viewport:', entry.target);
+ } else {
+ console.log('Element left viewport:', entry.target);
+ }
+});
+```
+
+### Setting a Custom Root Element
+
+You can observe intersections within a specific container by assigning the `root` attribute to the [root element's](https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserver/root) ID. Apply [`rootMargin`](https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserver/rootMargin) with the `root-margin` attribute to expand or contract the observation area.
+
+```html
+
+ ...
+
+```
+
+### Configuring Multiple Thresholds
+
+Track different visibility percentages by providing multiple [`threshold`](https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API#threshold) values as a space-separated list.
+
+```html
+ ...
+```
+
+### Applying Classes on Intersect
+
+The `intersect-class` attribute automatically toggles the specified class on direct children when they become visible. This enables pure CSS styling without JavaScript event handlers.
+
+```html {.example}
+
+
+ Fade In
+ Slide In
+ Scale & Rotate
+ Bounce
+
+
+
+Scroll to see elements transition at 50% visibility
+
+
+```
diff --git a/packages/webawesome/docs/docs/resources/changelog.md b/packages/webawesome/docs/docs/resources/changelog.md
index d5dd55807..f612b9714 100644
--- a/packages/webawesome/docs/docs/resources/changelog.md
+++ b/packages/webawesome/docs/docs/resources/changelog.md
@@ -18,6 +18,7 @@ Components with the Experimental badge sh
- Removed the `fixed-width` attribute as it's now the default behavior
- 🚨 BREAKING: Renamed the `icon-position` attribute to `icon-placement` in `` [discuss:1340]
- 🚨 BREAKING: Removed the `size` attribute from `` as it only set the initial size and gets out of sync when buttons are updated (apply a `size` to each button instead)
+- Added the `` component
- Added the Hindi translation [pr:1307]
- Added `--show-duration` and `--hide-duration` to `` [issue:1281]
- Fixed incorrectly named exported tooltip parts in `` [pr:1277]
diff --git a/packages/webawesome/src/components/intersection-observer/intersection-observer.css b/packages/webawesome/src/components/intersection-observer/intersection-observer.css
new file mode 100644
index 000000000..92d692cdd
--- /dev/null
+++ b/packages/webawesome/src/components/intersection-observer/intersection-observer.css
@@ -0,0 +1,3 @@
+:host {
+ display: contents;
+}
diff --git a/packages/webawesome/src/components/intersection-observer/intersection-observer.test.ts b/packages/webawesome/src/components/intersection-observer/intersection-observer.test.ts
new file mode 100644
index 000000000..0af25892d
--- /dev/null
+++ b/packages/webawesome/src/components/intersection-observer/intersection-observer.test.ts
@@ -0,0 +1,9 @@
+import { expect, fixture, html } from '@open-wc/testing';
+
+describe('', () => {
+ it('should render a component', async () => {
+ const el = await fixture(html` `);
+
+ expect(el).to.exist;
+ });
+});
diff --git a/packages/webawesome/src/components/intersection-observer/intersection-observer.ts b/packages/webawesome/src/components/intersection-observer/intersection-observer.ts
new file mode 100644
index 000000000..e703be0d0
--- /dev/null
+++ b/packages/webawesome/src/components/intersection-observer/intersection-observer.ts
@@ -0,0 +1,200 @@
+import { html } from 'lit';
+import { customElement, property } from 'lit/decorators.js';
+import { WaIntersectEvent } from '../../events/intersect.js';
+import { clamp } from '../../internal/math.js';
+import { parseSpaceDelimitedTokens } from '../../internal/parse.js';
+import { watch } from '../../internal/watch.js';
+import WebAwesomeElement from '../../internal/webawesome-element.js';
+import styles from './intersection-observer.css';
+
+/**
+ * @summary Tracks immediate child elements and fires events as they move in and out of view.
+ * @documentation https://webawesome.com/docs/components/intersection-observer
+ * @status stable
+ * @since 2.0
+ *
+ * @slot - Elements to track. Only immediate children of the host are monitored.
+ *
+ * @event {{ entry: IntersectionObserverEntry }} wa-intersect - Fired when a tracked element begins or ceases intersecting.
+ */
+@customElement('wa-intersection-observer')
+export default class WaIntersectionObserver extends WebAwesomeElement {
+ static css = styles;
+
+ private intersectionObserver: IntersectionObserver | null = null;
+ private observedElements = new Map();
+
+ /** Element ID to define the viewport boundaries for tracked targets. */
+ @property() root: string | null = null;
+
+ /** Offset space around the root boundary. Accepts values like CSS margin syntax. */
+ @property({ attribute: 'root-margin' }) rootMargin = '0px';
+
+ /** One or more space-separated values representing visibility percentages that trigger the observer callback. */
+ @property() threshold = '0';
+
+ /**
+ * CSS class applied to elements during intersection. Automatically removed when elements leave
+ * the viewport, enabling pure CSS styling based on visibility state.
+ */
+ @property({ attribute: 'intersect-class' }) intersectClass = '';
+
+ /** If enabled, observation ceases after initial intersection. */
+ @property({ type: Boolean, reflect: true }) once = false;
+
+ /** Deactivates the intersection observer functionality. */
+ @property({ type: Boolean, reflect: true }) disabled = false;
+
+ connectedCallback() {
+ super.connectedCallback();
+
+ if (!this.disabled) {
+ this.updateComplete.then(() => {
+ this.startObserver();
+ });
+ }
+ }
+
+ disconnectedCallback() {
+ super.disconnectedCallback();
+ this.stopObserver();
+ }
+
+ private handleSlotChange() {
+ if (!this.disabled) {
+ this.startObserver();
+ }
+ }
+
+ /** Converts threshold property string into numeric array. */
+ private parseThreshold(): number[] {
+ const tokens = parseSpaceDelimitedTokens(this.threshold);
+ return tokens.map((token: string) => {
+ const num = parseFloat(token);
+ return isNaN(num) ? 0 : clamp(num, 0, 1);
+ });
+ }
+
+ /** Locates and returns the root element using the specified ID. */
+ private resolveRoot(): Element | null {
+ if (!this.root) return null;
+
+ try {
+ const doc = this.getRootNode() as Document | ShadowRoot;
+ const target = doc.getElementById(this.root);
+
+ if (!target) {
+ console.warn(`Root element with ID "${this.root}" could not be found.`, this);
+ }
+
+ return target;
+ } catch {
+ console.warn(`Invalid selector for root: "${this.root}"`, this);
+ return null;
+ }
+ }
+
+ /** Initializes or reinitializes the intersection observer instance. */
+ private startObserver() {
+ this.stopObserver();
+
+ // Skip setup if functionality is disabled
+ if (this.disabled) return;
+
+ // Convert threshold string to numeric values
+ const threshold = this.parseThreshold();
+
+ // Locate the root boundary element
+ const rootElement = this.resolveRoot();
+
+ // Set up unified observer for all child elements
+ this.intersectionObserver = new IntersectionObserver(
+ entries => {
+ entries.forEach(entry => {
+ const wasIntersecting = this.observedElements.get(entry.target) ?? false;
+ const isIntersecting = entry.isIntersecting;
+
+ // Record current intersection state
+ this.observedElements.set(entry.target, isIntersecting);
+
+ // Toggle intersection class based on visibility
+ if (this.intersectClass) {
+ if (isIntersecting) {
+ entry.target.classList.add(this.intersectClass);
+ } else {
+ entry.target.classList.remove(this.intersectClass);
+ }
+ }
+
+ // Emit the intersection event
+ const changeEvent = new WaIntersectEvent({ entry });
+ this.dispatchEvent(changeEvent);
+
+ if (isIntersecting && !wasIntersecting) {
+ // When once mode is active, cease tracking after first intersection
+ if (this.once) {
+ this.intersectionObserver?.unobserve(entry.target);
+ this.observedElements.delete(entry.target);
+ }
+ }
+ });
+ },
+ {
+ root: rootElement,
+ rootMargin: this.rootMargin,
+ threshold,
+ },
+ );
+
+ // Begin tracking all immediate child elements
+ const slot = this.shadowRoot!.querySelector('slot');
+ if (slot !== null) {
+ const elements = slot.assignedElements({ flatten: true });
+ elements.forEach(element => {
+ this.intersectionObserver?.observe(element);
+ // Set initial non-intersecting state
+ this.observedElements.set(element, false);
+ });
+ }
+ }
+
+ /** Halts the intersection observer and cleans up. */
+ private stopObserver() {
+ // Clear intersection classes from all tracked elements before stopping
+ if (this.intersectClass) {
+ this.observedElements.forEach((_, element) => {
+ element.classList.remove(this.intersectClass);
+ });
+ }
+
+ this.intersectionObserver?.disconnect();
+ this.intersectionObserver = null;
+ this.observedElements.clear();
+ }
+
+ @watch('disabled', { waitUntilFirstUpdate: true })
+ handleDisabledChange() {
+ if (this.disabled) {
+ this.stopObserver();
+ } else {
+ this.startObserver();
+ }
+ }
+
+ @watch('root', { waitUntilFirstUpdate: true })
+ @watch('rootMargin', { waitUntilFirstUpdate: true })
+ @watch('threshold', { waitUntilFirstUpdate: true })
+ handleOptionsChange() {
+ this.startObserver();
+ }
+
+ render() {
+ return html` `;
+ }
+}
+
+declare global {
+ interface HTMLElementTagNameMap {
+ 'wa-intersection-observer': WaIntersectionObserver;
+ }
+}
diff --git a/packages/webawesome/src/events/events.ts b/packages/webawesome/src/events/events.ts
index e9ef29957..341f6f608 100644
--- a/packages/webawesome/src/events/events.ts
+++ b/packages/webawesome/src/events/events.ts
@@ -11,6 +11,7 @@ export type { WaExpandEvent } from './expand.js';
export type { WaFinishEvent } from './finish.js';
export type { WaHideEvent } from './hide.js';
export type { WaHoverEvent } from './hover.js';
+export type { WaIntersectEvent } from './intersect.js';
export type { WaInvalidEvent } from './invalid.js';
export type { WaLazyChangeEvent } from './lazy-change.js';
export type { WaLazyLoadEvent } from './lazy-load.js';
diff --git a/packages/webawesome/src/events/intersect.ts b/packages/webawesome/src/events/intersect.ts
new file mode 100644
index 000000000..01da80ada
--- /dev/null
+++ b/packages/webawesome/src/events/intersect.ts
@@ -0,0 +1,19 @@
+/** Emitted when an element's intersection state changes. */
+export class WaIntersectEvent extends Event {
+ readonly detail?: WaIntersectEventDetail;
+
+ constructor(detail?: WaIntersectEventDetail) {
+ super('wa-intersect', { bubbles: false, cancelable: false, composed: true });
+ this.detail = detail;
+ }
+}
+
+interface WaIntersectEventDetail {
+ entry?: IntersectionObserverEntry;
+}
+
+declare global {
+ interface GlobalEventHandlersEventMap {
+ 'wa-intersect': WaIntersectEvent;
+ }
+}