Visibility detection using Svelte 
Tags: svelte, javascript, webdev, beginners
Published: 2020-03-06 07:35:01

I've been playing around with [Svelte](https://svelte.dev/) lately and I really enjoy it. If you haven't heard of Svelte before I highly recommend watching [Rethinking reactivity](https://www.youtube.com/watch?v=AdNJ3fydeao) from the author Rich Harris.

Svelte compiles into imperative and performant JavaScript, it uses no Virtual DOM diffing like many other frameworks. The compilation step will strip out features and styles that aren't used so it won't get into the final bundle. But the best thing about Svelte could be the expressiveness of the framework 😍.

> **TLDR** Visibility detection
- Lazy load: https://svelte.dev/repl/97df8ddcd07a434890ffb38ff8051291?version=3.19.1
- A little crazy: https://svelte.dev/repl/6d5c36ae0d2647298f0485b00b9dbfa9?version=3.19.1
- With transitions: https://svelte.dev/repl/7bc94b49825f47728444fe8b0ed943cc?version=3.19.2


## Project setup
Some of you might already be familiar with Svelte, some might not, I will try explain as much as I can so it's easy to follow along. 

To create a new Svelte project:


```
npx degit sveltejs/template my-svelte-project
```

This uses the default sveltejs template to generate the project files. You can change to folder name if you like. Our new project should look something like this:

```
├── README.md
├── package.json
├── public
│   ├── favicon.png
│   ├── global.css
│   └── index.html
├── rollup.config.js
└── src
    ├── App.svelte
    └── main.js
```

Now within the project folder we should start by installing all the dependencies. 

```
npm install

⸨       ░░░░░░░░░░░⸩⸩ :. extract:svelte: ...
```

With our dependencies installed we can start our dev-server.

```
npm run dev

Your application is ready~! 🚀

- Local:      http://localhost:5000
```

Now we are up and running, you can visit localhost:5000 and you'll see a simple Hello World application.


## What we are going to build
It's not uncommon to defer loading content until it's visible to the user, often referred to as lazy loading. To be able to lazy load we need a way to detect when elements are in screen.

Maybe we can aim to build a general purpose Visibility detector and through it see how Svelte can interact with web api's like the [InterSectionObserver](https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API). Maybe we can make it reusable and flexible enough for different uses cases including but not limited to lazy loading.


Let's create a new file in the src directory called **Visibility.svelte** that will hold the code for our Visibility detection.

```
:
└── src
    ├── Visibility.svelte <-- NEW FILE
    ├── App.svelte
    └── main.js
```

Our Visibility component will make use of the IntersectionObserver and through it we will register an element and detect when it intersects with the viewport of our document.

It can take a threshold option and with it we can configure which parts in the intersection we are interested in.  

Say we provide an array of `[0, 0.5, 1]` for the threshold, this would result in events when it starts to intersect (0), when 50% of our element is visible (0,5) and when the element is completely visible (1).

![Alt Intersection Observer illustration](https://dev-to-uploads.s3.amazonaws.com/i/b2k5l6w143lwirmn8eh8.gif)


## Code walkthrough
In Svelte the `JavaScript` is put within a `<script>` tag. Most of the `js` code will work just as expected but there's also some Svelte specific functionality which I'll try to explain.

This component doesn't have any styling, if it did it would be put within a `<style>` tag and be scoped to the component.

The HTML elements are put in the document just as you would in a regular `index.html` file.


**Visbility.svelte**
```html
<script>
    import {onMount} from 'svelte';

    export let top = 0;
    export let bottom = 0;
    export let left = 0;
    export let right = 0;

    export let steps = 100;

    let element;
    let percent;
    let observer;
    let unobserve = () => {};
    let intersectionObserverSupport = false;

    function intersectPercent(entries) {
        entries.forEach(entry => {
            percent = Math.round(Math.ceil(entry.intersectionRatio * 100));
        })
    }

    function stepsToThreshold(steps) {
        return [...Array(steps).keys()].map(n => n / steps)
    }

    onMount(() => {
        intersectionObserverSupport =
                'IntersectionObserver' in window &&
                'IntersectionObserverEntry' in window &&
                'intersectionRatio' in window.IntersectionObserverEntry.prototype;

        const options = {
            rootMargin: `${top}px ${right}px ${bottom}px ${left}px`,
            threshold: stepsToThreshold(steps)
        };

        if (intersectionObserverSupport) {
            observer = new IntersectionObserver(intersectPercent, options);
            observer.observe(element);
            unobserve = () => observer.unobserve(element);
        }

        return unobserve;
    });
</script>

<div bind:this={element}>
    <slot {percent} {unobserve}/>
</div>

```

In Svelte whenever we want to expose properties in our component we use `export let <property name>` and if we assign it a value it will act as the default if it's not passed in.

The first collection of properties we expose is `top`, `left`, `bottom` and `right`. These are offset values for the intersection container, they adjust the placement of the "box" that our elements will pass through.

The next property we have is `steps`, this is just a number we are going to use to create thresholds, if we set it to 100 it would create `[0.01, 0.02 ... 0.98, 0.99, 1]`. If we have 100 steps then events will be emitted whenever the visibility changes by one percent.

Elements we want to observe needs to be in the DOM, so we import the life cycle method `onMount`from Svelte. It takes a callback that's invoked when the DOM is ready. And if we return a function from the `onMount` callback it gets invoked when the component is destroyed. 

We return the `unobserve` function to ensure it is called at end of the component life cycle. Now before we continue I would like to show how this component would be used. 


````html
import Visibility from `Visibility.svelte`

<Visibility steps={100} let:percent let:unobserve}>
  {#if percent > 50}
    <h1 use:unobserve>Hello world</h1>
  {/if}
</Visibility>
```

In the code above we has access to `percent` and `unobserve` (I'll come back to how this works in a moment). The `use:unobserve` is called an action in Svelte, the action will be invoked when the if statement is true, the `<h1>` gets created and we unobserve the container that wraps our content.


```html
<div bind:this={element}>
 <slot {percent} {unobserve} />
</div>
```

Behind the scenes this is wired up with a div using `bind:this={element}` directive. This allow us to get a reference to the DOM element and bind it to the variable inside the brackets.

The reason we use a wrapping div is because the InterSectionObserver needs a DOM element and we're not allowed to use directives on the slot itself.

The slot item will take on the content we pass into the `<Visibility>` tag. The `{percent}` and `{unregister}` is slot properties, this is why we can access them in the parent through `let:percent` and `let:unregister`.

That's all we need for flexible lazy loading. Now we have steps set to 100, it's not required though, we could use `steps={2}` to get `[0, 0.5]` and it would still work.

The example above is very simple and might be a little tricky to see what's going on. We would need to put the content off screen and maybe visualize the precent somehow. 

We can use the [Online REPL](https://svelte.dev/repl/). Here's one example that lazy loads an image once the visibility hits 70 percent: https://svelte.dev/repl/97df8ddcd07a434890ffb38ff8051291?version=3.19.1

And here's the code, the reload function is just for convenience so we can easily try it out more than once. The whole `<Visibility>` block will be recreated when hitting reload. 


```html
<script>
    import Visibility from './Visibility.svelte'

    let show = true;

    function reload() {
        show = false;
        setTimeout(() => show = true, 100)
    }
</script>

<style>
    main {
        text-align: center;
    }

    h1 {
        letter-spacing: .1rem;
        margin-bottom: 100vh;
    }

    section {
        display: flex;
        align-items: center;
        justify-content: center;
        margin-bottom: 10rem;
        padding: 1rem;
        position: relative;
        box-shadow: 0 0 10px -5px black;
        height: 300px;
    }

    img {
        height: 300px;
        width: 300px;
    }

    .top-left, .top-right, .bottom-left, .bottom-right {
        position: absolute;
        background: yellow;
        padding: .5rem;
        font-size: .8rem;
        font-weight: 700;
    }

    .top-left {
        top: 0;
        left: 0;
    }

    .top-right {
        top: 0;
        right: 0;
    }

    .bottom-left {
        bottom: 0;
        left: 0;
    }

    .bottom-right {
        bottom: 0;
        right: 0;
    }
</style>

<main>
    <button on:click={reload}>Reload</button>
    <h1>Scroll down</h1>

    {#if show}
        <Visibility steps={100} let:percent let:unobserve>
            <section>
                <span class="top-left">{percent}%</span>
                <span class="top-right">{percent}%</span>
                <span class="bottom-left">{percent}%</span>
                <span class="bottom-right">{percent}%</span>


                {#if percent > 70}
                    <img alt="Robot"
                         use:unobserve
                         src="https://robohash.org/svelte-is-awesome.png">
                {/if}
            </section>
        </Visibility>
    {/if}
</main>

```

## Let's get a little crazy  
We could keep the observer running and hook into the percent values we get and use it to create some new styles: https://svelte.dev/repl/6d5c36ae0d2647298f0485b00b9dbfa9?version=3.19.1


```html
<script>
    import Visibility from './Visibility.svelte'

    function getStyle(percent) {
        return `
            opacity: ${percent/100};
            transform: rotate(${percent * 3.6}deg) scale(${percent/100});
        `
    }
</script>

<!-- styles here, omitted for brevity -->

<main>
    <h1>Scroll down</h1>

    <Visibility steps={100} let:percent let:unobserve>
        <section style="{getStyle(percent)}">
            <span class="top-left">{percent}%</span>
            <span class="top-right">{percent}%</span>
            <span class="bottom-left">{percent}%</span>
            <span class="bottom-right">{percent}%</span>

            <figure>
                <img alt="Robot"
                     src="https://robohash.org/svelte-is-awesome.png">
            </figure>
        </section>
    </Visibility>
</main>
```

Here we modify the DOM with new styles, but we could also make use of svelte transitions and I encourage you to. These will get translated to css animations that runs off the main thread.

Let's see a quick example: https://svelte.dev/repl/7bc94b49825f47728444fe8b0ed943cc?version=3.19.2

```html
<script>
    import Visibility from './Visibility.svelte'
    import {fly} from 'svelte/transition';
</script>

<!-- styles here, omitted for brevity -->

<main>
    <h1>Scroll down</h1>

    <Visibility steps={100} let:percent let:unobserve>
        <section>
            <span class="top-left">{percent}%</span>
            <span class="top-right">{percent}%</span>
            <span class="bottom-left">{percent}%</span>
            <span class="bottom-right">{percent}%</span>

            {#if percent > 70}
                <img alt="Robot"
                     in:fly={{duration: 1000, x: -500}}
                     out:fly={{duration: 500, y: -500}}
                     src="https://robohash.org/svelte-is-awesome.png">
            {/if}
        </section>
    </Visibility>
</main>
```

You can see this in action, if open up your Dev tools and inspect the `<img>` element and you should see something like `style="animation: 1000ms linear 0ms 1 normal both running __svelte_701641917_0;"`


## Wrapping up

I will continue to explore Svelte and I hope you will too. There's a lot I haven't talked about, one thing that comes to mind is the reactivity and the `$:` symbol. 

But I highly recommend going through the [Online Tutorial](https://svelte.dev/tutorial) because it will explain it way better than I could and you'll be able to code along in the REPL.