SECRET OF CSS

Create a Popover In Svelte. How to create a popover in Svelte… | by David Dal Busco | Jun, 2022


How to create a popover in Svelte without any other libraries or dependencies.

0*7fX6Ojn4lPRXVlXu
Photo by Nick Fewings on Unsplash

I have implemented the UI components of my last project Papyrs without any third party design system libraries — i.e. I created all the components from scratch. I did so to get full control and flexibility over the miscellaneous bricks of my opinionated layout.

In this blog post, I share how you can develop a popover component in Svelte.

​A popover is a floating container that is rendered over the content next to an anchor — commonly a button — which initiates its display. To improve the visual focus of the overlay, a backdrop is generally used to partially obfuscate the view behind it.

0*MsjQuhNr ifgJL0D

We can start the implementation by replicating above skeleton in a component named Popover.svelte​​ which contains a button​ and div​.

<button>Open</button>

<div
role="dialog"
aria-labelledby="Title"
aria-describedby="Description"
aria-orientation="vertical"
>
<div>Backdrop</div>
<div>Content</div>
</div>

To improve the accessibility we can set the dialog​ role and provide some aria​ information (see MDN docs for more details).

We create a boolean state — ​ visible​ — to display or close the popover. When the button​ is clicked, the state is set to true​ and the overlay gets rendered. On the contrary, when the backdrop is clicked, it turns to false​ and it closes.

​In addition, we add a click listener on the popover that does nothing else than stopping the event propagation. This is useful to avoid closing the overlay when user is interacting with its content.

We can also make the overlay appearing and disappearing gracefully thanks to the transition directive — also known as “Svelte’s black magic” 😁.

<script lang="ts">
import { fade, scale } from 'svelte/transition';
import { quintOut } from 'svelte/easing';

let visible = false;
</script>

<button on:click={() => (visible = true)}>Open</button>

{#if visible}
<div
role="dialog"
aria-labelledby="Title"
aria-describedby="Description"
aria-orientation="vertical"
transition:fade
on:click|stopPropagation
>
<div
on:click|stopPropagation={() => (visible = false)}
transition:scale={{ delay: 25, duration: 150, easing: quintOut }}
>
Backdrop
</div>
<div>Content</div>
</div>
{/if}

​The popover should be rendered over the all content regardless if the page has been scrolled or not. Therefore we can use a fixed​ position as starting point. Its content and backdrop are both set to an absolute​ positioning. The backdrop should cover the screen as well but is a child of the overlay — therefore the “absolute” — and the content should be positioned next to the anchor.

​The rest of the CSS code we add to our solution are minimal styling settings for width, height or colors.

<script lang="ts">
import { fade, scale } from 'svelte/transition';
import { quintOut } from 'svelte/easing';

let visible = false;
</script>

<button on:click={() => (visible = true)}>Open</button>

{#if visible}
<div
role="dialog"
aria-labelledby="Title"
aria-describedby="Description"
aria-orientation="vertical"
transition:fade
class="popover"
on:click|stopPropagation
>
<div
on:click|stopPropagation={() => (visible = false)}
transition:scale={{ delay: 25, duration: 150, easing: quintOut }}
class="backdrop"
/>
<div class="wrapper">Content</div>
</div>
{/if}

<style>
.popover {
position: fixed;
inset: 0;

z-index: 997;
}

.backdrop {
position: absolute;
inset: 0;

background: rgba(0, 0, 0, 0.3);
}

.wrapper {
position: absolute;

min-width: 200px;
max-width: 200px;

min-height: 100px;

width: fit-content;
height: auto;

overflow: hidden;

display: flex;
flex-direction: column;
align-items: flex-start;

background: white;
color: black;
}
</style>

​To set the overlay next to the button we have to get a reference on that element to find its position within the viewport. For such purpose we can bind​ the anchor.

When the reference is ready or when the window is resized (the position might change if user resize the browser), we use the getBoundingClientRect() method to query the information about the position.

We ultimately translate these JavaScript information to CSS variables to render the content of the popover at the exact position we would like to set.

<script lang="ts">
// ...

let anchor: HTMLButtonElement | undefined = undefined;

let bottom: number;
let left: number;

const initPosition = () =>
({ bottom, left } = anchor?.getBoundingClientRect() ?? { bottom: 0, left: 0 });

$: anchor, initPosition();
</script>

<svelte:window on:resize={initPosition} />

<button on:click={() => (visible = true)} bind:this={anchor}>Open</button>

{#if visible}
<div
role="dialog"
aria-labelledby="Title"
aria-describedby="Description"
aria-orientation="vertical"
transition:fade
class="popover"
on:click|stopPropagation
style="--popover-top: {`${bottom}px`}; --popover-left: {`${left}px`}"
>
<!-- ... -->
</div>
{/if}

<style>
/** ... */

.wrapper {
position: absolute;

top: calc(var(--popover-top) + 10px);
left: var(--popover-left);

/** ... */
}
</style>

​Above code snippet was trimmed to showcase only what is related to this chapter. Altogether, the component’s code is the following:

<script lang="ts">
import { fade, scale } from 'svelte/transition';
import { quintOut } from 'svelte/easing';

let visible = false;
let anchor: HTMLButtonElement | undefined = undefined;

let bottom: number;
let left: number;

const initPosition = () =>
({ bottom, left } = anchor?.getBoundingClientRect() ?? { bottom: 0, left: 0 });

$: anchor, initPosition();
</script>

<svelte:window on:resize={initPosition} />

<button on:click={() => (visible = true)} bind:this={anchor}>Open</button>

{#if visible}
<div
role="dialog"
aria-labelledby="Title"
aria-describedby="Description"
aria-orientation="vertical"
transition:fade
class="popover"
on:click|stopPropagation
style="--popover-top: {`${bottom}px`}; --popover-left: {`${left}px`}"
>
<div
on:click|stopPropagation={() => (visible = false)}
transition:scale={{ delay: 25, duration: 150, easing: quintOut }}
class="backdrop"
/>
<div class="wrapper">Content</div>
</div>
{/if}

<style>
.popover {
position: fixed;
inset: 0;

z-index: 997;
}

.backdrop {
position: absolute;
inset: 0;

background: rgba(0, 0, 0, 0.3);
}

.wrapper {
position: absolute;

top: calc(var(--popover-top) + 10px);
left: var(--popover-left);

min-width: 200px;
max-width: 200px;

min-height: 100px;

width: fit-content;
height: auto;

overflow: hidden;

display: flex;
flex-direction: column;
align-items: flex-start;

background: white;
color: black;
}
</style>

And…that’s it! We have implemented a bare minimum custom popover that can be used in any Svelte applications without any dependency.

​We did not beautify the solution, therefore the outcome remains visually rough on the edges but, the popover works out as expected.

To iterate from there, implement more options or make it shiny, you could for example have a look to the open source code of Papyrs on GitHub 🤗.

​To infinity and beyond
David​

1*eth8UohB1g TeamISklW A
Want to Connect?For more adventures, follow me on Twitter





News Credit

%d bloggers like this: