Javascript is required
8 min read

Building a Polite Newsletter Popup With Nuxt 3

Building a Polite Newsletter Popup With Nuxt 3 Image

This year I launched a free eBook with 27 helpful tips for Vue developers for subscribers of my weekly Vue newsletter. For marketing purposes, I showed a popup on the landing page of my portfolio page each time a user visited my site. I was aware that users probably could get annoyed by that popup. Thus I added a "Don't show again" button to that popup. I thought I solved the problem!

But soon, I realized that many other sites solved that problem more elegantly. If you visit their website, stay for some time, and scroll through the content, a small notification appears at the bottom of the screen. It asks if you are interested in a specific product, and if you agree, it redirects to a page with information about this product.

In this article, I'll explain how I built a polite popup to ask people if they would like to subscribe to my newsletter using Nuxt 3.

What is a polite popup?

Photo by Emily Morter on Unsplash

The goal of a so-called polite popup is to only ask for visitors emails if it detects that visitors are engaged with your content. This means they’ll be more likely to sign up by the time we ask them because it’ll be after they’ve decided they liked our content.

In the following sections, we'll build a popup that

  • waits for a visitor to browse the website
  • makes sure visitors are interested in the website
  • appears off to the side in a non-intrusive way
  • is easy to dismiss or ignore
  • asks for permission first
  • waits a bit before it appears again


Now that we know the criteria of a polite popup let's start implementing it using Nuxt 3.


I use Nuxt.js in this example, but the concepts and solutions are not tied to any framework.

The demo code is interactively available at StackBlitz:

Implement the composable

The most exciting challenge of the polite popup is only showing it if visitors are interested in the website and content we want to promote.

Technically, we'll solve it this way:

  • The visitor must be visiting a page with Vue-related content as my newsletter targets Vue developers.
  • The visitor must be actively scrolling the current page for 6 seconds or more.
  • The visitor must scroll through at least 35% of the current page during their visit.


If these numbers aren’t generating the amount of engagement you want to see from visitors, you can enable a more aggressive mode that will lower the threshold by about 20-30%.

Let's start by writing a Vue composable for our polite popup:

1import { useWindowScroll, useWindowSize, useTimeoutFn } from '@vueuse/core'
3const config = {
4  timeoutInMs: 3000,
5  contentScrollThresholdInPercentage: 300,
8export const usePolitePopup = () => {
9  const visible = useState('visible', () => false)
10  const readTimeElapsed = useState('read-time-elapsed', () => false)
12  const { start } = useTimeoutFn(
13    () => {
14      readTimeElapsed.value = true
15    },
16    config.timeoutInMs,
17    { immediate: false }
18  )
19  const { y: scrollYInPx } = useWindowScroll()
20  const { height: windowHeight } = useWindowSize()
22  // Returns percentage scrolled (ie: 80 or NaN if trackLength == 0)
23  const amountScrolledInPercentage = computed(() => {
24    const documentScrollHeight = document.documentElement.scrollHeight
25    const trackLength = documentScrollHeight - windowHeight.value
26    const percentageScrolled = Math.floor((scrollYInPx.value / trackLength) * 100)
27    return percentageScrolled
28  })
30  const scrolledContent = computed(() => amountScrolledInPercentage.value >= config.contentScrollThresholdInPercentage)
32  const trigger = () => {
33    readTimeElapsed.value = false
34    start()
35  }
37  watch([readTimeElapsed, scrolledContent], ([newReadTimeElapsed, newScrolledContent]) => {
38    if (newReadTimeElapsed && newScrolledContent) {
39      visible.value = true
40    }
41  })
43  return {
44    visible,
45    trigger,
46  }

We defined two state variables:

  • visible: a boolean indicating if the popup should be visible or not.
  • readTimeElapsed: a boolean indicating if the user has spent the defined time on the page.

The trigger method is exposed and triggers the a timer which is used to check if the visitor has spent a predefined amount of time on the page. A Vue watcher is used to set visible to true if the timer has expired and the scroll threshold is exceeded. For the timer, we use VueUse's useTimeoutFn composable which runs a setTimeout function and sets the readTimeElapsed state variable to true after the timer has expired.

Let's take a detailed look at the amountScrolledInPercentage computed property:

1import { useWindowSize } from '@vueuse/core'
3const { height: windowHeight } = useWindowSize()
5// Returns percentage scrolled (ie: 80 or NaN if trackLength == 0)
6const amountScrolledInPercentage = computed(() => {
7  const documentScrollHeight = document.documentElement.scrollHeight
8  const trackLength = documentScrollHeight - windowHeight.value
9  const percentageScrolled = Math.floor((scrollYInPx.value / trackLength) * 100)
10  return percentageScrolled

To get the total scrollable area of a document, we need to retrieve the following two measurements of the page:

  1. The height of the browser window: We use VueUse's useWindowSize composable to get reactive variable of the browser window height.
  2. The height of the entire document: We use document.documentElement.scrollHeight to get the height of the document, including content not visible on the screen due to overflow.

By subtracting 2 from 1, we get the total scrollable area of the document. VueUse's useWindowScroll composable is used to access the number of pixels the document is currently scrolled along the vertical axis.

Move your eyes down to the trackLength variable, which gets the total available scroll length of the document. The variable will contain 0 if the page is not scrollable. The percentageScrolled variable then divides the scrollYInPx variable (amount the user has scrolled) with trackLength to derive how much the user has scrolled percentage wise.

Finally, we need to trigger the popup on certain pages. In our case, we only want to trigger it on the Vue route path:

2  <main>
3    <ContentDoc />
4  </main>
7<script setup lang="ts">
8const route = useRoute()
10const { trigger } = usePolitePopup()
12if (route.path === '/vue') {
13  trigger()

Write the popup component

Now it's time to write the Vue component that renders the popup: