Tracking Page Views in SvelteKit: Intersection Observer
Tags: svelte, webdev, javascript
Published: 2021-09-27 08:49:46

---
title: "Tracking Page Views in SvelteKit: Intersection Observer"
published: "true"
description: "Tracking page views in SvelteKit: how you can add a view counter to your blog posts using the Intersection Observer API as a view trigger."
tags: "svelte, webdev, javascript"
canonical_url: "https://rodneylab.com/tracking-page-views-sveltekit/"
cover_image: "https://dev-to-uploads.s3.amazonaws.com/uploads/articles/lw3wm997hww31srmit3h.png"
---

## 🖱 Tracking Scroll Events in SvelteKit

In this post we look at how to make tracking page views in SvelteKit easy.  You might have a blog and want to add a view counter to improve user experience.  You might also want to keep track of which articles or pages on a website get read right to the end.  This will give stakeholders a better impression of what content works well.  Although we focus on a page view example, the techniques we look at here can be used to track a wider set of scroll events.  As an example, you may want to <a aria-label="Learn how you can lazy load iframes in Svelte Kit" href="https://rodneylab.com/lazy-loading-iframes-sveltekit/">know when an iframe is about to come into the visible viewport to trigger a lazy load</a>.  Finally, you might want to change a header component based on which section of the page the user is currently viewing.  All of these problems can be solved using the Intersection Observer API.

In our example we will consider the page viewed once the user has scrolled the post banner image completely out of view.

![Tracking Page Views in SvelteKit: Intersection Observer: Screenshot show top of blog post page with a navigation bar up top and a large postbanner image just below](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/1ll1uy9g9dftddhb4yg6.png)

## 🔭 Intersection Observer API

Although the <a href="https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API" aria-label="Learn about the Intersection Observer on M D N">Intersection Observer API</a> was introduced to make tracking scroll event simpler, it can be a little daunting, so we will try to break it down here.  We will end with some SvelteKit code which you can use as a starting point for your own projects.

### `iframe` Example

Essentially we use the Intersection Observer API, to tell us whether an element is in view or not.  This element would be the iframe in the case we were lazy loading an iframe and wanted to know when it was in view.  As well as the element we are tracking we have a reference frame, the root element.

By default, the root element is the viewport.  So we track whether the observed element (the iframe in our example) is inside the root element.  In the case of a lazy loading iframe, if we wanted to maximise user experience, we would start lazy loading the iframe before it came into the root element (the viewport in our case).  To do this we might say trigger the lazy load when the iframe is within 100&nbsp;px of the bottom of the root element, so it is not yet visible, but will be visible as soon a the user scrolls up just anothe 100&nbsp;pixels.  In this case the `rootMargin` parameter is helpful.

### Ad Example

With the iframe example, we want to trigger as soon as the first pixel of the iframe enters our (extended) root element.  If we were placing an ad on our site and want to record the number of views of the ad, we might consider the add viewed once say 90% of it is visible in the viewport.  Here we would not need to extend the root element as with the iframe.  But we would want to trigger once 90% was in view, rather than the very first pixel and can do this by using the `threshold` parameter.

One thing to note on the Intersection Observer is that it is triggered in either direction.  Meaning, by default, with the iframe example.  With the iframe initially out of view, the user scrolls down and the event is triggered (iframe switches from being outside the root element to being inside). If the user now scrolls up again, a fresh event is triggered (iframe switches from being inside the reference element to outside).

Equally, when the iframe is in view and the user scrolls right down to the bottom of the page, so the iframe is no longer visible, another event is triggered (iframe switches from being inside the root element to being outside).  Taking this into account, depending on the use case, you probably want to disable the observer once the first event is triggered.  In the iframe example, you only need to lazy load it once!  In the ad example, the advertiser might accuse you of fraud if you count a view (and bill them for it) when the ad enters the viewport and another when it leaves!

### `rootMargin`

Root margin can be used to grow or shrink the root element.  Think of it like a CSS margin when you set parameters.  That said, you can only specify in units of pixels or a percentage (also, be sure to write `0px`, rather than just `0`).  Why would you want to grow or shrink the root element?  By default the root element is the visible viewport.  If we want to the observer to trigger a lazy load of an iframe, it makes sense to trigger before the iframe enters the viewport, to give it time to load and improve user experience.  Here growing the root element helps.  Let's say we went for this:

```javascript
const options = {
  rootMargin: '0px 0px 100px'
}
```

We interpret this as we would a CSS margin, so the first `0px` means apply a top margin of zero (i.e. do nothing with the top of the root element).  The second `0px` refers to left and right margin, again we do nothing.  The `100px` refers to the bottom margin.  We are saying grow the root element by shifting the bottom of it out 100&nbsp;pixels.  This is just what we need; by growing the root element, we can trigger the observation earlier and anticipate the iframe coming into the view, getting ready a touch sooner.

Remember this parameter works like a CSS margin so a negative value will decrease the size of the root element while a positive value increases it.

### `threshold`

The `threshold` option just controls how much of the observed element needs to be visible for an event to be triggered.  For the iframe example, we can keep it at the default 0, meaning as soon as the first pixel enters the root element, we trigger an observation.  For the ad example, we might try somehting like:

```javascript
const options = {
  rootMargin: '0px',
  threshold: 0.9
}
```

Here we need to have the observed element 90% visible to trigger.  Rememember that triggers work both ways.  So if we are scrolling the observer element into view and it goes from the top 89% being visible to the top 91% being visible, we have a trigger.  If we continue scrolling, we might get to a point where only the bottom 91% is visible.  If we continue scrolling, we will trigger another event once less than the bottom 90% is visible.

Hope that I explained it well enough!  Let me know if there's some element I can improve on.  That's enough theory for now.  Let's code up an example.

## 🧱 Tracking Page Views in SvelteKit

Let's leave behind our iframe and ad examples and look at a page view.  We have a blog and want to know how many times each post gets viewed.  We could trigger a view as soon as the page loads.  Though what happens if the user clicked the wrong link and immediately presses the back button?  We would count a view, when the user didn't event read the first sentence.

In reality you would want to trigger a view once the user scrolls past let's say 25%, 50% or 75% of the article into view.  You would choose the threshold based best-suited to your needs. We'll keep it simple here.  We assume you write semantic HTML and have exactly one `main` element on your blog post pages.  We trigger a view once the user scrolls the first child element of the main element out of view.  So let's say we have a structure somthing like this:

```html
<main>
	<picture>
		...
		<img ...>
	</picture>
	<h1>Article Title</h1>
	<p>First sentence</p>
}
```

The first child element of the main element is the picture, so once the user scrolls past that, we trigger a view.

![Tracking Page Views in SvelteKit: Intersection Observer: Screenshot show top of blog post page with a navigation bar up top and a large post banner image just below](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/1ll1uy9g9dftddhb4yg6.png)

Now we know what our metric is, let's write some Svelte!  We'll create a component just for the intersection observer and place it in its own file.  Although the content is in a `.svelte` file, it will not actually render anything in our example.


```html
<script>
  import { onMount, onDestroy } from 'svelte';
  import { browser } from '$app/env';
 
  function handleView() {
    alert('Intersection Observer view event triggered');
  }
 
  let observer;
 
  onMount(() => {
    if (browser) {
      const handleIntersect = (entries, observer) => {
        entries.forEach((entry) => {
          if (entry.isIntersecting) {
            observer.unobserve(entry.target);
            handleView();
          }
        });
      };
      const options = { threshold: 1, rootMargin: '100% 0% -100%' };
      observer = new IntersectionObserver(handleIntersect, options);
      const element = window.document.querySelector('main').firstElementChild;
      observer.observe(element);
    }
  });
 
  onDestroy(() => {
    if (observer) {
      observer.disconnect();
    }
  });
</script>
```

### A Closer Look at the Code

This is not as daunting as it might first look.  Let's break it down and see why.  First we import `onMount`, `onDestroy` and `browser`.  You might already know `browser` is a SvelteKit inbuilt boolean which returns true when our code is running in the browser and false, on the server.  `onMount` and `onDestroy` let us create code that only needs to be run once, as the component is created or once no longer needed.

The `handleView` function is lines `5`&ndash;`7` contains the code we would normally run on a view.  This would involve updating the view counter in the UI and also letting the database know there was a new view.

We will create an `observer` variable and want to access it both in `onMount` and in `onDestroy`.  For that reason we declare it without assigning a value outside of both those functions, so that in can de accessed from within them.


#### Intersection Observer Options
The `onMount` function contains the substance of our component.  First let's look at line&nbsp;`21`.  Here we define the options.  We set a threshold of 1, meaning we trigger the intersection when we go from the picture being less than 100% visible to being 100% visible or vice versa. This does not sound like it will do what we want it to but let's carry on anyway.

Interestingly, we are increasing the top margin by 100% (also in line `21`), this makes the root element bigger.  So if we have a viewport height of 812&nbsp;px, our root element now starts 812&nbsp;px above the top of the viewport and ends at the bottom of the view port.  Next, we make no change to the left and right root margin, but decrease the bottom margin by 100%.  Now the bottom margin essentially moves to the top of the view port.

What have we done here? We have shifted the entire root element so that it is off screen, standing on top of the viewport.  This is actually quite convenient for our use case.  Remember we want to know when our observed element scrolls off the top of the visible viewport.  Now (because of our margin adjustments), when that happens, the entire element will be in our shifted rootElement.  When the last pixel of the picture scrolls up out of view, 100% of the picture will be in our shifted root element.  This is why we set the trigger to 1 &mdash;  once 100% of the picture is in the  shifted rootElement, we want to trigger an intersection.

#### Creating an Intersection Observer

In line `22` we create the Intersection Observer, using the options we just defined.  As well as the options we pass a callback function.  This is called when an intersection is observed. The next lines find the element we want to observe and tell the Intersection Observer to observe it.

#### Intersection Observer Callback
Finally, we have our callback function: `handleIntersect`.  The API passes in two parameters which we will use: `entries` and `observer`.  Entries is an array, in our case it will only ever have one entry.  That is because we defined a single threshold.  You can define `threshold` as an array though (let's say you want to know when 25%, 50% and 75% of the element is visible) and be able to discern which threshold was triggered in the callback.

Line `16` is quite important as it tells the observer to stop observing, once we have an intersection.  We only need to count a view once the picture first scrolls out of view.  If the user scrolls to the top of the page again, we don't need to count another view.  Once the first view is counted, the observer has done its work and can chill!

Equally important is remembering to use our intersection event.  In line `17` we call our `handleView` function.  In a real-world application, this would add a new view to our database.

## 💯 Testing it Out

We can test the component by <a aria-label="Open the Svelte Kit M D sveX starter repo on Git Hub" href="https://github.com/rodneylab/sveltekit-blog-mdx">cloning the SvelteKit MDsveX starter</a>, adding the new component and then adding the component to the rendered content in the BlogPost template.  Let's do that quickly now.

<h3>How to Track Page Views in SvelteKit</h3>

<ol><li id="how-to-track-page-views-in-svelte-kit-1-1">Clone the MDsveX blog starter and spin up a local dev server:<figure class="CodeFragment-module--container--3N_ub "><pre class="prism-code language-jsx" tabindex="0" style="overflow-y: auto;"><div class="token-line"><div class=""><div class="CodeFragment-module--line--3C-lF "><span class="CodeFragment-module--line-content--215kv"><span class="token plain">git clone https</span><span class="token operator">:</span><span class="token operator">/</span><span class="token operator">/</span><span class="token plain">github</span><span class="token punctuation">.</span><span class="token plain">com</span><span class="token operator">/</span><span class="token plain">rodneylab</span><span class="token operator">/</span><span class="token plain">sveltekit</span><span class="token operator">-</span><span class="token plain">blog</span><span class="token operator">-</span><span class="token plain">mdx</span><span class="token punctuation">.</span><span class="token plain">git sveltekit</span><span class="token operator">-</span><span class="token plain">intersection</span><span class="token operator">-</span><span class="token plain">observer</span></span></div></div></div><div class="token-line"><div class=""><div class="CodeFragment-module--line--3C-lF "><span class="CodeFragment-module--line-content--215kv"><span class="token plain">cd sveltekit</span><span class="token operator">-</span><span class="token plain">intersection</span><span class="token operator">-</span><span class="token plain">observer</span></span></div></div></div><div class="token-line"><div class=""><div class="CodeFragment-module--line--3C-lF "><span class="CodeFragment-module--line-content--215kv"><span class="token plain">cp </span><span class="token punctuation">.</span><span class="token plain">env</span><span class="token punctuation">.</span><span class="token constant">EXAMPLE</span><span class="token plain"> </span><span class="token punctuation">.</span><span class="token plain">env</span></span></div></div></div><div class="token-line"><div class=""><div class="CodeFragment-module--line--3C-lF "><span class="CodeFragment-module--line-content--215kv"><span class="token plain">pnpm install # or npm install</span></span></div></div></div><div class="token-line"><div class=""><div class="CodeFragment-module--line--3C-lF "><span class="CodeFragment-module--line-content--215kv"><span class="token plain">pnpm run dev</span></span></div></div></div></pre></figure></li><li id="how-to-track-page-views-in-svelte-kit-1-2">Create a new file <code>src/lib/components/IntersectionObserver.svelte</code> and paste in the code block above.</li><li id="how-to-track-page-views-in-svelte-kit-1-3">Edit the <code>src/lib/components/BlogPost.svelte</code> component to import the IntersectionObserver component and add it to the DOM:<figure class="CodeFragment-module--container--3N_ub CodeFragment-module--container-with-title--3wMRG"><div class="CodeFragment-module--title-container--b4gbS"><span class="CodeFragment-module--title-text--2qfxE">src/lib/components/BlogPost.svelte</span><div class="CodeFragment-module--language-tag--PpiAf">jsx</div></div><pre class="prism-code language-jsx" tabindex="0" style="overflow-y: auto;"><div class="token-line"><div class=""><div class="CodeFragment-module--line--3C-lF "><span class="CodeFragment-module--line-number--1xoCV ">1</span><span class="CodeFragment-module--line-content--215kv"><span class="token tag punctuation">&lt;</span><span class="token tag">script</span><span class="token tag punctuation">&gt;</span><span class="token plain"></span></span></div></div></div><div class="token-line"><div class=""><div class="CodeFragment-module--line--3C-lF "><span class="CodeFragment-module--line-number--1xoCV ">2</span><span class="CodeFragment-module--line-content--215kv"><span class="token plain">  </span><span class="token keyword">import</span><span class="token plain"> readingTime </span><span class="token keyword">from</span><span class="token plain"> </span><span class="token string">'reading-time'</span><span class="token punctuation">;</span><span class="token plain"></span></span></div></div></div><div class="token-line"><div class=""><div class="CodeFragment-module--line--3C-lF "><span class="CodeFragment-module--line-number--1xoCV ">3</span><span class="CodeFragment-module--line-content--215kv"><span class="token plain">  </span><span class="token keyword">import</span><span class="token plain"> BannerImage </span><span class="token keyword">from</span><span class="token plain"> </span><span class="token string">'$lib/components/BannerImage.svelte'</span><span class="token punctuation">;</span><span class="token plain"></span></span></div></div></div><div class="token-line"><div class="CodeFragment-module--line-container-highlit--2Yivn"><div class="CodeFragment-module--line--3C-lF CodeFragment-module--line-highlighted--3edH4"><span class="CodeFragment-module--line-number--1xoCV CodeFragment-module--line-number-highlighted--2ykbA">4</span><span class="CodeFragment-module--line-content--215kv"><span class="token plain">  </span><span class="token keyword">import</span><span class="token plain"> IntersectionObserver </span><span class="token keyword">from</span><span class="token plain"> </span><span class="token string">'$lib/components/IntersectionObserver.svelte'</span><span class="token punctuation">;</span><span class="token plain"></span></span></div></div></div><div class="token-line"><div class=""><div class="CodeFragment-module--line--3C-lF "><span class="CodeFragment-module--line-number--1xoCV ">5</span><span class="CodeFragment-module--line-content--215kv"><span class="token plain">  </span><span class="token keyword">import</span><span class="token plain"> </span><span class="token constant">SEO</span><span class="token plain"> </span><span class="token keyword">from</span><span class="token plain"> </span><span class="token string">'$lib/components/SEO/index.svelte'</span><span class="token punctuation">;</span></span></div></div></div></pre></figure><figure class="CodeFragment-module--container--3N_ub CodeFragment-module--container-with-title--3wMRG"><div class="CodeFragment-module--title-container--b4gbS"><span class="CodeFragment-module--title-text--2qfxE">src/lib/components/BlogPost.svelte</span><div class="CodeFragment-module--language-tag--PpiAf">jsx</div></div><pre class="prism-code language-jsx" tabindex="0" style="overflow-y: auto;"><div class="token-line"><div class="CodeFragment-module--line-container-highlit--2Yivn"><div class="CodeFragment-module--line--3C-lF CodeFragment-module--line-highlighted--3edH4"><span class="CodeFragment-module--line-number--1xoCV CodeFragment-module--line-number-highlighted--2ykbA">72</span><span class="CodeFragment-module--line-content--215kv"><span class="token tag punctuation">&lt;</span><span class="token tag class-name">IntersectionObserver</span><span class="token tag"> </span><span class="token tag punctuation">/&gt;</span><span class="token plain"></span></span></div></div></div><div class="token-line"><div class=""><div class="CodeFragment-module--line--3C-lF "><span class="CodeFragment-module--line-number--1xoCV ">73</span><span class="CodeFragment-module--line-content--215kv"><span class="token plain"></span><span class="token operator">&lt;</span><span class="token plain">BannerImage </span><span class="token punctuation">{</span><span class="token plain">imageData</span><span class="token punctuation">}</span><span class="token plain"> </span><span class="token operator">/</span><span class="token operator">&gt;</span><span class="token plain"></span></span></div></div></div><div class="token-line"><div class=""><div class="CodeFragment-module--line--3C-lF "><span class="CodeFragment-module--line-number--1xoCV ">74</span><span class="CodeFragment-module--line-content--215kv"><span class="token plain"></span><span class="token tag punctuation">&lt;</span><span class="token tag">h1</span><span class="token tag"> </span><span class="token tag attr-name">class</span><span class="token tag attr-value punctuation attr-equals">=</span><span class="token tag attr-value punctuation">"</span><span class="token tag attr-value">heading</span><span class="token tag attr-value punctuation">"</span><span class="token tag punctuation">&gt;</span><span class="token punctuation">{</span><span class="token plain">title</span><span class="token punctuation">}</span><span class="token tag punctuation">&lt;/</span><span class="token tag">h1</span><span class="token tag punctuation">&gt;</span></span></div></div></div></pre></figure></li><li id="how-to-track-page-views-in-svelte-kit-1-4">Navigate to a blog post on the dev site and scroll past a picture, an alert should appear.  You can now customise the code, adding a counter to the DOM and hooking up the <code>handleView</code> function in the Intersection Observer component to your database.</li></ol>

There is a <a aria-label="Open the demo code on the Rodney Lab Git Hub site" href="https://github.com/rodneylab/sveltekit-intersection-observer">full working example on the Rodney Lab GitHub page</a>.  As well as this I have <a aria-label="Open the demo site" href="https://sveltekit-intersection-observer.rodneylab.com/">deployed a full working demo</a>. I hope all the steps above are clear and you know have a working knowledge of the Intersection Observer API and how to use it in SvelteKit.  If there is any way I could improve on this post, please drop a comment below or get in touch.  Also check out the <a aria-label="Open the M D N docs on the Intersection Observer  A P I" href="https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API">MDN docs on the Intersection Observer API</a>.  I deliberately explained it a little differently here so that you can use those docs to complement the explanation above.  They have a nice animation which might bring it home, if you are not yet 100% comfortable.

## 🙏🏽 Feedback

Have you found the post useful? Do you have your own methods for solving this problem? Let me know your solution. Would you like to see posts on another topic instead? Get in touch with ideas for new posts. Also if you like my writing style, get in touch if I can write some posts for your company site on a consultancy basis. Read on to find ways to get in touch, further below. If you want to support posts similar to this one and can spare a few dollars, euros or pounds, please <a aria-label="Support Rodney Lab via Buy me a Coffee" href="https://rodneylab.com/giving/">consider supporting me through Buy me a Coffee</a>.

Finally, feel free to share the post on your social media accounts for all your followers who will find it useful. As well as leaving a comment below, you can get in touch via <a aria-label="Message me on Twitter" href="https://twitter.com/messages/compose?recipient_id=1323579817258831875">@askRodney</a> on Twitter and also <a aria-label="Contact Rodney Lab via Telegram" href="https://t.me/askRodney">askRodney on Telegram</a>. Also, see <a aria-label="Get in touch with Rodney Lab" href="https://rodneylab.com/contact">further ways to get in touch with Rodney Lab</a>. I post regularly on <Link aria-label="See posts on svelte kit" href="https://rondeylab.com/tags/sveltekit/">SvelteKit</Link> as well as other topics. Also <a aria-label="Subscribe to the Rodney Lab newsletter" href="https://rodneylab.com/about/#newsletter">subscribe to the newsletter to keep up-to-date</a> with our latest projects.