diff --git a/packages/react/src/hooks/index.ts b/packages/react/src/hooks/index.ts
index b04e2456a..9b026ea79 100644
--- a/packages/react/src/hooks/index.ts
+++ b/packages/react/src/hooks/index.ts
@@ -24,6 +24,7 @@ export { usePreviousValue } from './usePreviousValue';
export { useResizeObserver } from './useResizeObserver';
export { useScrollDirection } from './useScrollDirection';
export { useScrollLock } from './useScrollLock';
+export { useScrollSpy } from './useScrollSpy';
export { useScrollSync } from './useScrollSync';
export { useSessionStorage } from './useSessionStorage';
export { useSticky } from './useSticky';
diff --git a/packages/react/src/hooks/useScrollSpy/index.ts b/packages/react/src/hooks/useScrollSpy/index.ts
new file mode 100644
index 000000000..8c7b41831
--- /dev/null
+++ b/packages/react/src/hooks/useScrollSpy/index.ts
@@ -0,0 +1 @@
+export { useScrollSpy } from './useScrollSpy';
diff --git a/packages/react/src/hooks/useScrollSpy/useScrollSpy.api.mdx b/packages/react/src/hooks/useScrollSpy/useScrollSpy.api.mdx
new file mode 100644
index 000000000..6a291ef2a
--- /dev/null
+++ b/packages/react/src/hooks/useScrollSpy/useScrollSpy.api.mdx
@@ -0,0 +1,26 @@
+import { Meta } from '@storybook/addon-docs';
+import LinkTo from '@storybook/addon-links/react';
+import { TableFunction } from '~storybook/components/TableFunction';
+
+
+
+# useScrollSpy API
+
+```js
+import { useScrollSpy } from '@elonkit/react';
+```
+
+
+
+
+
+
+## Demos
+
+
diff --git a/packages/react/src/hooks/useScrollSpy/useScrollSpy.stories.tsx b/packages/react/src/hooks/useScrollSpy/useScrollSpy.stories.tsx
new file mode 100644
index 000000000..ee6b2d721
--- /dev/null
+++ b/packages/react/src/hooks/useScrollSpy/useScrollSpy.stories.tsx
@@ -0,0 +1,74 @@
+import { useRef } from 'react';
+
+import { Meta, StoryObj } from '@storybook/react';
+
+import { useScrollSpy } from './useScrollSpy';
+
+import { Button } from '../../components';
+
+const sections = ['1', '2', '3', '4', '5'];
+
+const meta: Meta = {
+ tags: ['autodocs'],
+ title: 'Hooks/useScrollSpy',
+ parameters: {
+ references: ['useScrollSpy'],
+ },
+};
+
+export default meta;
+
+type Story = StoryObj;
+
+export const Demo: Story = {
+ render: function Render() {
+ const containerRef = useRef(null);
+
+ const isDocsPage = window.location.href.includes('docs');
+
+ const activeId = useScrollSpy(sections, {
+ ...(containerRef.current ? { root: containerRef.current } : {}),
+ });
+
+ return (
+
+
+ {sections.map((section) => (
+
+ ))}
+
+
+ {sections.map((section, index) => (
+
+ ))}
+
+
+ );
+ },
+};
diff --git a/packages/react/src/hooks/useScrollSpy/useScrollSpy.ts b/packages/react/src/hooks/useScrollSpy/useScrollSpy.ts
new file mode 100644
index 000000000..e4cf038b8
--- /dev/null
+++ b/packages/react/src/hooks/useScrollSpy/useScrollSpy.ts
@@ -0,0 +1,38 @@
+import { useEffect, useState } from 'react';
+
+import { useCallbackThrottle } from '../useCallbackThrottle';
+import { useEvent } from '../useEvent';
+
+/**
+ * The hook that observes the intersection of elements with an ancestor or viewport and determines the active one.
+ * @param items An array of ids of elements to be observed.
+ * @param options An options object allowing you to set options for the observation.
+ * @param throttleDelay The time in milliseconds used to throttle the observation.
+ */
+export const useScrollSpy = (items: string[], options?: IntersectionObserverInit, throttleDelay = 0): string => {
+ const [activeElementId, setActiveElementId] = useState('');
+
+ const callback = useEvent(
+ useCallbackThrottle((entries: IntersectionObserverEntry[]) => {
+ entries.forEach((e) => e.isIntersecting && setActiveElementId(e.target.id));
+ }, throttleDelay)
+ );
+
+ useEffect(() => {
+ const observer = new IntersectionObserver(callback, options);
+
+ items.forEach((id) => {
+ const element = document.getElementById(id);
+
+ if (element) {
+ observer.observe(element);
+ }
+ });
+
+ return () => {
+ observer.disconnect();
+ };
+ }, [items]);
+
+ return activeElementId;
+};