+
+[](https://npmjs.org/package/react-intersection-observer)
+[](https://github.com/thebuilder/react-intersection-observer/actions?query=workflow%3ATest)
+[](LICENSE)
+[](http://npm-stat.com/charts.html?package=react-intersection-observer)

A React implementation of the [Intersection Observer API](https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API)
@@ -259,9 +261,9 @@ extend the `IntersectionObserverEntry` with the `isVisible` boolean.
The `IntersectionObserver` itself is just a simple but powerful tool. Here's a
few ideas for how you can use it.
-- [Lazy image load](docs/Recipes.md#lazy-image-load)
-- [Trigger animations](docs/Recipes.md#trigger-animations)
-- [Track impressions](docs/Recipes.md#track-impressions) _(Google Analytics, Tag
+- [Lazy image load](apps/docs/docs/guides/recipes.mdx#lazy-image-loading)
+- [Trigger animations](apps/docs/docs/guides/recipes.mdx#scroll-triggered-animation)
+- [Track impressions](apps/docs/docs/guides/recipes.mdx#track-an-impression) _(Google Analytics, Tag
Manager, etc.)_
## FAQ
@@ -375,7 +377,7 @@ globally, alongside either `jest.fn` or `vi.fn`.
See the instructions for [Vitest](#vitest). You should be able to use a similar
setup/reset code, adapted to the testing library you are using. Failing that,
-copy the code from [test-utils.ts][test-utils-url], and make your own version.
+copy the code from [test-utils.ts](packages/react-intersection-observer/src/test-utils.ts), and make your own version.
### Fallback Behavior
@@ -574,13 +576,3 @@ order to destroy the observer again.
> You most likely won't need this, but it can be useful if you
> need to handle IntersectionObservers outside React, or need full control over
> how instances are created.
-
-[package-url]: https://npmjs.org/package/react-intersection-observer
-[npm-version-svg]: https://img.shields.io/npm/v/react-intersection-observer.svg
-[license-image]: http://img.shields.io/npm/l/react-intersection-observer.svg
-[license-url]: LICENSE
-[downloads-image]: http://img.shields.io/npm/dm/react-intersection-observer.svg
-[downloads-url]: http://npm-stat.com/charts.html?package=react-intersection-observer
-[test-image]: https://github.com/thebuilder/react-intersection-observer/workflows/Test/badge.svg
-[test-url]: https://github.com/thebuilder/react-intersection-observer/actions?query=workflow%3ATest
-[test-utils-url]: https://github.com/thebuilder/react-intersection-observer/blob/master/src/test-utils.ts
diff --git a/apps/docs/public/logo-horizontal.svg b/apps/docs/public/logo-horizontal.svg
new file mode 100644
index 00000000..a25fb66d
--- /dev/null
+++ b/apps/docs/public/logo-horizontal.svg
@@ -0,0 +1,20 @@
+
+
diff --git a/apps/storybook/.storybook/main.ts b/apps/storybook/.storybook/main.ts
index 57c841a0..2135815c 100644
--- a/apps/storybook/.storybook/main.ts
+++ b/apps/storybook/.storybook/main.ts
@@ -1,12 +1,29 @@
import type { StorybookConfig } from "@storybook/react-vite";
+const favicon = ``;
+
const config: StorybookConfig = {
framework: "@storybook/react-vite",
+ staticDirs: [{ from: "../../docs/public", to: "/" }],
stories: [
"../stories/**/*.mdx",
"../stories/**/*.@(story|stories).@(ts|tsx)",
],
addons: ["@storybook/addon-docs", "@storybook/addon-vitest"],
+ // Storybook's generated brandImage rule uses !important, so this needs to be
+ // appended after it and match that priority.
+ managerHead: (head) => `
+ ${head}
+ ${favicon}
+
+ `,
+ previewHead: (head) => `${head}${favicon}`,
core: {
builder: "@storybook/builder-vite",
},
diff --git a/apps/storybook/.storybook/manager.ts b/apps/storybook/.storybook/manager.ts
index 51a50669..191a850c 100644
--- a/apps/storybook/.storybook/manager.ts
+++ b/apps/storybook/.storybook/manager.ts
@@ -1,10 +1,12 @@
import { addons } from "storybook/manager-api";
-import { themes } from "storybook/theming";
+import docsLogo from "../../docs/public/logo-horizontal.svg";
+import { storybookTheme } from "./theme";
addons.setConfig({
theme: {
- ...themes.dark,
- brandTitle: "React IntersectionObserver",
- brandUrl: "https://github.com/thebuilder/react-intersection-observer",
+ ...storybookTheme,
+ brandImage: docsLogo,
+ brandTitle: "React Intersection Observer",
+ brandUrl: "https://react-intersection-observer.vercel.app/",
},
});
diff --git a/apps/storybook/.storybook/preview.ts b/apps/storybook/.storybook/preview.ts
index 18645f25..ac85b998 100644
--- a/apps/storybook/.storybook/preview.ts
+++ b/apps/storybook/.storybook/preview.ts
@@ -1,14 +1,12 @@
-import { themes } from "storybook/theming";
+import { storybookTheme } from "./theme";
import "../styles.css";
export const parameters = {
controls: {
expanded: true,
},
- theme: {
- ...themes.dark,
- },
+ theme: storybookTheme,
docs: {
- theme: themes.dark,
+ theme: storybookTheme,
},
};
diff --git a/apps/storybook/.storybook/theme.ts b/apps/storybook/.storybook/theme.ts
new file mode 100644
index 00000000..78ebd21e
--- /dev/null
+++ b/apps/storybook/.storybook/theme.ts
@@ -0,0 +1,12 @@
+import { themes } from "storybook/theming";
+
+// sRGB equivalent of the docs light-mode accent: oklch(0.6 0.15 290).
+// Storybook's theme engine does not support oklch() values.
+const docsAccent = "#816dd2";
+
+export const storybookTheme = {
+ ...themes.light,
+ barSelectedColor: docsAccent,
+ colorPrimary: docsAccent,
+ colorSecondary: docsAccent,
+};
diff --git a/apps/storybook/stories/Intro.mdx b/apps/storybook/stories/Intro.mdx
index 28cf3d66..29ac5db1 100644
--- a/apps/storybook/stories/Intro.mdx
+++ b/apps/storybook/stories/Intro.mdx
@@ -1,4 +1,7 @@
-import { Markdown } from '@storybook/addon-docs/blocks';
-import Readme from '../../../README.md?raw';
+# Explore the API
-{Readme}
+A React implementation of the [Intersection Observer API](https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API).
+
+Use the stories in this library to explore the `InView` component, `useInView`, and `useOnInView` in isolation.
+
+For installation, API reference, guides, and recipes, visit the [documentation site](https://react-intersection-observer.vercel.app/).
diff --git a/apps/storybook/stories/Recipes.mdx b/apps/storybook/stories/Recipes.mdx
deleted file mode 100644
index 0851bbd8..00000000
--- a/apps/storybook/stories/Recipes.mdx
+++ /dev/null
@@ -1,4 +0,0 @@
-import { Markdown } from '@storybook/addon-docs/blocks';
-import Recipes from '../../../docs/Recipes.md?raw';
-
-{Recipes}
diff --git a/docs/Recipes.md b/docs/Recipes.md
deleted file mode 100644
index 9f1bffd5..00000000
--- a/docs/Recipes.md
+++ /dev/null
@@ -1,143 +0,0 @@
-# Recipes
-
-The `IntersectionObserver` itself is just a simple but powerful tool. Here's a
-few ideas for how you can use it.
-
-## Lazy image load
-
-It's actually easy to create your own lazy image loader, and this allows you to
-build it according to your needs.
-
-**Couple of tips**
-
-- Don't set the `src` (or `srcset`) on the image until it's visible. Images will
- always load their `src`, even if you set `display: none;`.
-- Make sure to set the
- [root margin](https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserver/rootMargin)
- for top and bottom, so the Intersection Observer gets triggered before the
- image enters the viewport. This gives the image a chance to be loaded before
- the user even sees it. Try to start with something like `200px 0px`, but
- experiment with it until you find the right value.
-- Set `triggerOnce`, so you don't keep monitoring for changes.
-- You should always create a wrapping element, that sets the correct aspect
- ratio for the image. You can set the padding bottom to be
- `${height / width * 100}%` to maintain aspect ratio.
-- Either hide the `` with CSS, or skip rendering it until it's inside the
- viewport.
-
-> [!TIP]
-> All modern browsers support the native `loading` attribute on `` tags, so unless you need
-> fine-grained control, you can skip the `IntersectionObserver` and use `loading="lazy"` instead.
->
-> https://web.dev/articles/browser-level-image-lazy-loading
-
-```jsx
-import React from "react";
-import { useInView } from "react-intersection-observer";
-
-const LazyImage = ({ width, height, src, ...rest }) => {
- const { ref, inView } = useInView({
- triggerOnce: true,
- rootMargin: "200px 0px",
- });
-
- return (
-
- {inView ? (
-
- ) : null}
-
- );
-};
-
-export default LazyImage;
-```
-
-**See [Codesandbox](https://codesandbox.io/embed/lazy-image-load-mjsgc)**
-
-## Trigger animations
-
-Triggering animations once they enter the viewport is also a perfect use case
-for an IntersectionObserver.
-
-- Set `triggerOnce`, to only trigger the animation the first time.
-- Set `threshold`, to control how much of the element should be visible before
- firing the event.
-- Instead of `threshold`, you can use `rootMargin` to have a fixed amount be
- visible before triggering. Use a negative margin value, like `-100px 0px`, to
- have it go inwards. You can also use a percentage value, instead of pixels.
-
-```jsx
-import React from "react";
-import { useInView } from "react-intersection-observer";
-
-const LazyAnimation = () => {
- const { ref, inView } = useInView({
- triggerOnce: true,
- rootMargin: "-100px 0px",
- });
-
- return (
-
- 👋
-
- );
-};
-
-export default LazyAnimation;
-```
-
-## Track impressions
-
-You can use `IntersectionObserver` to track when a user views your element, and
-fire an event on your tracking service. Consider using the `useOnInView` to
-trigger changes via a callback.
-
-- Set `triggerOnce`, to only trigger an event the first time the element enters
- the viewport.
-- Set `threshold`, to control how much of the element should visible before
- firing the event.
-- Instead of `threshold`, you can use `rootMargin` to have a fixed amount be
- visible before triggering. Use a negative margin value, like `-100px 0px`, to
- have it go inwards. You can also use a percentage value, instead of pixels.
-
-```jsx
-import * as React from "react";
-import { useOnInView } from "react-intersection-observer";
-
-const TrackImpression = () => {
- const ref = useOnInView((inView) => {
- if (inView) {
- // Fire a tracking event to your tracking service of choice.
- dataLayer.push("Section shown"); // Here's a GTM dataLayer push
- }
- }, {
- triggerOnce: true,
- rootMargin: "-100px 0",
- });
-
- return (
-
- Exemplars sunt zeluss de bassus fuga. Credere velox ducunt ad audax amor.
-