🖱 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 know when an iframe is about to come into the visible viewport to trigger a lazy load. 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.
🔭 Intersection Observer API #
Although the Intersection Observer API 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
maximize 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 px of the bottom of the root element, so it is not yet visible, but will be
visible as soon the user scrolls up just another 100 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 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:
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 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 something like:
Here, we need to have the observed element 90% visible to trigger. Remember, 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.
🗳 Poll #
🧱 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 trigger a view once the user scrolls the heading out of view. So let’s say we have a structure something like this:
So once the user scrolls past the h1
heading, we trigger a view.
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. We will wrap the element we want to
observe (the heading) in a new IntersectionObserver component. This will contain a slot
element, which actual render the observed element:
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
– 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 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 22
. Here we define the options. We
set a threshold of 1, meaning we trigger the intersection when we go from the heading 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 px, our root
element now starts 812 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 heading will be in our shifted root element. This is why we set the trigger to 1 — once 100% of the picture is in the shifted rootElement, we want to trigger an intersection.
Creating an Intersection Observer #
In line 23
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 line just tells the Intersection Observer which element to observe. We have bound
this element to the container
variable, using bind:this
in line 35
. This saves us placing an id
on the element and attaching it using a query selector.
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 17
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 heading 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 18
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 cloning the SvelteKit MDsveX starter , adding the new component and then adding the component to the rendered content in the BlogPost template. Let’s do that quickly now.
How to Track Page Views in SvelteKit #
-
Clone the MDsveX blog starter and spin up a local dev server:
-
Create a new file
src/lib/components/IntersectionObserver.svelte
and paste in the code block above. -
Edit the
src/lib/components/BlogPost.svelte
component to import the IntersectionObserver component and add it to the DOM: - Navigate to a blog post on the dev site and scroll past a picture, an alert should appear. You can now customize the code, adding a counter to the DOM and hooking up the `handleView` function in the Intersection Observer component to your database.
There is a full working example on the Rodney Lab GitHub page . As well as this I have deployed a full working demo . 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 MDN docs on the Intersection Observer API . 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 consider supporting me through Buy me a Coffee.
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 @askRodney on Twitter and also askRodney on Telegram . Also, see further ways to get in touch with Rodney Lab. I post regularly on SvelteKit as well as other topics. Also, subscribe to the newsletter to keep up-to-date with our latest projects.