Day 28: Migrating Tooltips to Svelte

Hello again! I went on vacation without my laptop, but now it’s time to return, finish and ship the MVP version of Knowlo. I hope CofounderGPT had some time to rest and it’s ready to write some code.
I thought about our progress so far, and while I like the idea of using vanilla JavaScript for tooltips, it’s probably a good idea to use some light framework to simplify the maintenance in the future. Do we absolutely need a framework? Of course not! But it’s fun to try a new one, and it also simplifies onboarding new people.
Which one should we use? There’s no correct answer here. But I wanted to try the Svelte framework for some time, so this might be an excellent excuse.
Svelte and Web Components
What’s Svelte? Here’s CofounderGPT’s explanation:
Svelte is a modern JavaScript framework for building user interfaces, unlike traditional frameworks like React or Vue.js which do much of their work in the browser’s runtime, Svelte shifts this work into a compile-time process. When a Svelte application is built, the framework compiles your high-level components into efficient, imperative code that surgically updates the Document Object Model (DOM). This results in faster runtime performance, smaller bundle sizes, and a simplified developer experience. Svelte’s approach is to allow you to write declarative code, which is generally easier to read and maintain. Svelte is becoming increasingly popular in the web development community for its performance and simplicity.
While I don’t think the performance is the biggest bottleneck for Knowlo tooltips, it’s good to have a headstart. What’s more important for us is that Svelte is a light framework compared to some other popular options and can produce a Web Component with a relatively small bundle size.
The obvious next step is to ask CofounderGPT to convert our simple WebComponent to Svelte!
Slobodan: Can you convert a web component to Svelte? I’ll provide a code below.
CofounderGPT: Sure, please provide the web component code that you’d like to convert to Svelte.
Slobodan: Here’s the code. I need a TypeScript version. I am building a Web Component:
const projectId = document
.querySelector("[data-knowlo-project-id]")
.getAttribute("data-knowlo-project-id");
class KnowloTooltip extends HTMLElement {
constructor() {
super();
this.projectId = projectId;
this._show = this._show.bind(this);
this._hide = this._hide.bind(this);
this._hide();
const template = document.createElement("template");
template.innerHTML = `
<style>
@keyframes tooltipFadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
.tooltip:before {
content: "";
display: block;
width: 0;
height: 0;
border-style: solid;
border-right: 7px solid transparent;
border-left: 7px solid transparent;
border-bottom: 10px solid #ffffff;
border-top: 0;
transform: translateX(40px);
}
.tooltip {
position: absolute;
display: block;
max-width: 600px;
z-index: 20000;
animation: tooltipFadeIn 0.15s;
font-family: inherit;
}
.tooltip-container {
display: block;
overflow: hidden;
border-radius: 8px;
background: white;
box-shadow: 0px 2px 14px 0px rgba(0,0,0,0.39);
box-sizing: border-box;
}
.tooltip-content {
display: block;
overflow: hidden;
padding: 20px;
width: 100%;
border-radius: 8px 8px 0 0;
background: white;
box-sizing: border-box;
}
.tooltip-footer {
display: block;
overflow: hidden;
padding: 20px;
border-radius: 0 0 8px 8px;
background: #e5e7eb;
}
.tooltip-feedback {
display: block;
}
.tooltip-feedback button {
font-size: 18px;
}
.tooltip-feedback button i {
font-size: 18px;
}
.tooltip-read-more, .tooltip-qna {
margin-top: 0.5rem;
margin-bottom: 0.5rem;
}
.float-left {
float: left;
}
.float-right {
float: right;
}
.text-sm {
font-size: 0.75rem;
line-height: 1.25rem;
}
</style>
<div class="tooltip" data-knowlo-tooltip>
<div class="tooltip-container">
<div class="tooltip-content">
<div class="tooltip-text">
<p>The accruals feature allows admins to set up the frequency of days accrued based on the yearly leave quota. To enable accruals, follow these steps:</p>
<p>
<ol>
<li>Log in to the online dashboard at app.vacationtracker.io/signin.</li>
<li>Go to Settings and select Locations.</li>
<li>Choose the location you want to add accruals to.</li>
<li>Select the Leave Policies tab.</li>
<li>Click on the Edit icon next to the Leave Type.</li>
<li>In the Accruals section, set the frequency of accrued days. Make sure there is an amount in the Leave Quota section.</li>
<li>Choose one of the three accrual options: Monthly, Semimonthly, or Bi-weekly.</li>
<li>If you select Monthly or Semimonthly, the system will show a breakdown of the accrual period and the next accrual date. If you select Bi-weekly, choose the accrual day.</li>
<li>Click Update to save the accrual settings. Note that once configured, accrual settings cannot be changed.</li>
</ol>
</p>
</div>
<div class="tooltip-feedback float-right text-sm">
Is this information helpful?
<button><i class="fa fa-thumbs-up"></i></button>
<button><i class="fa fa-thumbs-down"></i></button>
</div>
</div>
<div class="tooltip-footer">
<div class="tooltip-read-more">Read more about accruals in Helpdesk <a class="external" href="#">How Do I Set Accruals?</a></div>
<div class="tooltip-qna">
<p>Or ask anything about this feature in this tooltip:</p>
<div class="chat">...</div>
<p class="text-sm float-left">This tooltip is powered by <a href="https://knowlo.co">Knowlo</a></p>
<p class="text-sm float-right">2/2 questions remaining</p>
</div>
</div>
</div>
</div>
`;
this.attachShadow({ mode: "open" });
this.shadowRoot.appendChild(template.content.cloneNode(true));
}
connectedCallback() {
if (!this.hasAttribute("role")) this.setAttribute("role", "tooltip");
if (!this.hasAttribute("tabindex")) this.setAttribute("tabindex", -1);
this._target = document.querySelector(
`[data-knowlo-tooltip-id="${this.id}"]`
);
if (!this._target) return;
this._target.addEventListener("focus", () => {
this._tooltipElementHasMouseOverOrFocus = true;
this._show();
});
this._target.addEventListener("blur", () => {
this._tooltipElementHasMouseOverOrFocus = false;
this._hide();
});
this._target.addEventListener("mouseenter", () => {
this._tooltipElementHasMouseOverOrFocus = true;
this._show();
});
this._target.addEventListener("mouseleave", () => {
this._tooltipElementHasMouseOverOrFocus = false;
this._hide();
});
this._tooltip = this.shadowRoot.querySelector("[data-knowlo-tooltip]");
this._tooltip.addEventListener(
"mouseenter",
() => (this._mouseIsOverTooltip = true)
);
this._tooltip.addEventListener("mouseleave", () => {
this._mouseIsOverTooltip = false;
this._hide();
});
}
disconnectedCallback() {
if (!this._target) return;
this._target.removeEventListener("focus", this._show);
this._target.removeEventListener("blur", this._hide);
this._target.removeEventListener("mouseenter", this._show);
this._target.removeEventListener("mouseleave", this._hide);
this._target = null;
this._tooltip.removeEventListener("mouseenter", this._show);
this._tooltip.removeEventListener("mouseleave", this._hide);
this._tooltip = null;
}
_show() {
this.hidden = false;
}
_hide() {
setTimeout(() => {
if (
!this._mouseIsOverTooltip &&
!this._tooltipElementHasMouseOverOrFocus
) {
this.hidden = true;
}
}, 10);
}
}
window.customElements.define("knowlo-tooltip", KnowloTooltip);
I used the older version of the WebComponent because of the context size.
CofounderGPT: You can convert your web component to a Svelte component with TypeScript, and it will look like this:
Create a KnowloTooltip.svelte
file:
<script lang="ts">
import { onMount, onDestroy } from "svelte";
let projectId = document
.querySelector("[data-knowlo-project-id]")
.getAttribute("data-knowlo-project-id");
let tooltipElementHasMouseOverOrFocus = false;
let mouseIsOverTooltip = false;
let target;
let tooltip;
const show = () => {
tooltip.hidden = false;
}
const hide = () => {
setTimeout(() => {
if (!mouseIsOverTooltip && !tooltipElementHasMouseOverOrFocus) {
tooltip.hidden = true;
}
}, 10);
}
onMount(() => {
if (!target.hasAttribute("role")) target.setAttribute("role", "tooltip");
if (!target.hasAttribute("tabindex")) target.setAttribute("tabindex", -1);
target.addEventListener("focus", () => {
tooltipElementHasMouseOverOrFocus = true;
show();
});
target.addEventListener("blur", () => {
tooltipElementHasMouseOverOrFocus = false;
hide();
});
target.addEventListener("mouseenter", () => {
tooltipElementHasMouseOverOrFocus = true;
show();
});
target.addEventListener("mouseleave", () => {
tooltipElementHasMouseOverOrFocus = false;
hide();
});
tooltip.addEventListener(
"mouseenter",
() => (mouseIsOverTooltip = true)
);
tooltip.addEventListener("mouseleave", () => {
mouseIsOverTooltip = false;
hide();
});
});
onDestroy(() => {
if (!target) return;
target.removeEventListener("focus", show);
target.removeEventListener("blur", hide);
target.removeEventListener("mouseenter", show);
target.removeEventListener("mouseleave", hide);
target = null;
tooltip.removeEventListener("mouseenter", show);
tooltip.removeEventListener("mouseleave", hide);
tooltip = null;
});
</script>
<div class="tooltip" data-knowlo-tooltip bind:this={tooltip}>
<!-- The rest of your HTML here -->
</div>
Make sure to place this component in a parent component where your data-knowlo-tooltip-id
attribute is defined.
Also, to reference styles in Svelte, you can either:
- Place them in a separate
.css
or.scss
file and import that file in your component. - Place them in a global stylesheet.
- Use Svelte’s scoped styling by including the
<style>
tag in your component. Note that Svelte’s default behavior is to scope styles to the current component, so if you want to use global styles, you would have to use the:global()
function.
If you want to have the styles local to the component (the default behavior in Svelte), you can just paste the CSS in between <style>
tags at the top of the component.
Slobodan: How do I place CSS in SCSS file? Convert our CSS to SCSS please.
CofounderGPT: Your current CSS code can be used as is in SCSS. SCSS (Sassy CSS) is a preprocessor of CSS that allows you to use features that don’t exist in CSS yet like variables, nesting, mixins, inheritance, and others.
However, we can still use some of the features of SCSS to improve the readability of your CSS. Here’s how we could refactor your CSS into SCSS using nesting:
@keyframes tooltipFadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
.tooltip {
position: absolute;
display: block;
max-width: 600px;
z-index: 20000;
animation: tooltipFadeIn 0.15s;
font-family: inherit;
&:before {
content: "";
display: block;
width: 0;
height: 0;
border-style: solid;
border-right: 7px solid transparent;
border-left: 7px solid transparent;
border-bottom: 10px solid #ffffff;
border-top: 0;
transform: translateX(40px);
}
}
.tooltip-container,
.tooltip-content {
display: block;
overflow: hidden;
box-sizing: border-box;
}
.tooltip-container {
border-radius: 8px;
background: white;
box-shadow: 0px 2px 14px 0px rgba(0,0,0,0.39);
}
.tooltip-content {
padding: 20px;
width: 100%;
border-radius: 8px 8px 0 0;
background: white;
}
.tooltip-footer {
display: block;
overflow: hidden;
padding: 20px;
border-radius: 0 0 8px 8px;
background: #e5e7eb;
}
.tooltip-feedback {
display: block;
font-size: 18px;
button {
font-size: 18px;
i {
font-size: 18px;
}
}
}
.tooltip-read-more, .tooltip-qna {
margin-top: 0.5rem;
margin-bottom: 0.5rem;
}
.float-left {
float: left;
}
.float-right {
float: right;
}
.text-sm {
font-size: 0.75rem;
line-height: 1.25rem;
}
Save this file with a .scss
extension, for example, tooltipStyles.scss
.
Then, you can import this stylesheet into your Svelte component as follows:
<script lang="ts">
import './tooltipStyles.scss';
// rest of your script code
</script>
<!-- rest of your Svelte code -->
Remember to configure your build tool (like rollup or webpack) to be able to process SCSS files.
Slobodan: Thanks! Can you give me the HTML content now?
CofounderGPT: Here’s the HTML content with the appropriate Svelte bindings.
<div class="tooltip-container">
<div class="tooltip-content">
<div class="tooltip-text">
<p>The accruals feature allows admins to set up the frequency of days accrued based on the yearly leave quota. To enable accruals, follow these steps:</p>
<p>
<ol>
<li>Log in to the online dashboard at app.vacationtracker.io/signin.</li>
<li>Go to Settings and select Locations.</li>
<li>Choose the location you want to add accruals to.</li>
<li>Select the Leave Policies tab.</li>
<li>Click on the Edit icon next to the Leave Type.</li>
<li>In the Accruals section, set the frequency of accrued days. Make sure there is an amount in the Leave Quota section.</li>
<li>Choose one of the three accrual options: Monthly, Semimonthly, or Bi-weekly.</li>
<li>If you select Monthly or Semimonthly, the system will show a breakdown of the accrual period and the next accrual date. If you select Bi-weekly, choose the accrual day.</li>
<li>Click Update to save the accrual settings. Note that once configured, accrual settings cannot be changed.</li>
</ol>
</p>
</div>
<div class="tooltip-feedback float-right text-sm">
Is this information helpful?
<button><i class="fa fa-thumbs-up"></i></button>
<button><i class="fa fa-thumbs-down"></i></button>
</div>
</div>
<div class="tooltip-footer">
<div class="tooltip-read-more">Read more about accruals in Helpdesk <a class="external" href="#">How Do I Set Accruals?</a></div>
<div class="tooltip-qna">
<p>Or ask anything about this feature in this tooltip:</p>
<div class="chat">...</div>
<p class="text-sm float-left">This tooltip is powered by <a href="https://knowlo.co">Knowlo</a></p>
<p class="text-sm float-right">2/2 questions remaining</p>
</div>
</div>
</div>
You can replace ...
with the appropriate chat system you want to implement. Note that this HTML is to be put within the <div class="tooltip" data-knowlo-tooltip bind:this={tooltip}>
element in your Svelte component.
Just ensure you have FontAwesome imported to your project since you’re using fa-thumbs-up
and fa-thumbs-down
classes for the icons in the buttons.
Slobodan: Excellent! How do I use this WebComponent in my svelte app?
CofounderGPT: Once you’ve created your Svelte component, you can use it in another Svelte component or Svelte application like you would use a regular HTML element.
First, import your newly created Svelte component into the parent component where you want to use it.
<script>
import KnowloTooltip from './KnowloTooltip.svelte';
</script>
Then, you can use the component in your markup just like a regular HTML tag:
<KnowloTooltip id="my-tooltip" />
Note the id
attribute. Your tooltip listens to events on a target element with a data-knowlo-tooltip-id
that matches the tooltip’s id. So you would also need to have an element in your DOM with that attribute:
<button data-knowlo-tooltip-id="my-tooltip">Hover me for tooltip</button>
<KnowloTooltip id="my-tooltip" />
In this case, when you hover over the button, the tooltip with the corresponding id will show up.
Slobodan: I got the “ReferenceError: document is not defined” error.
CofounderGPT: This error typically occurs in server-side rendering (SSR) environments, where code that assumes the presence of document
or window
(browser-only globals) is executed on the server. Since there’s no document
or window
object on the server, this error is thrown.
You are seeing this error because the document
object is being referenced outside of the component’s lifecycle methods. Here, document
is being used to retrieve the projectId
before the component’s constructor method is called, which is not allowed in Svelte’s SSR.
Here’s a simple way to handle this issue:
let projectId = ''
onMount(() => {
projectId = document.querySelector("[data-knowlo-project-id]").getAttribute("data-knowlo-project-id");
});
The onMount
function is imported from svelte
and it’s run after the component is first rendered in the DOM, and by that time document
would be defined. Please remember to import onMount
from svelte
:
import { onMount } from 'svelte';
Remember that, after moving the projectId
definition inside the onMount
, you’ll also need to move its usage inside the KnowloTooltip
class definition as well, and ensure you adjust the scope accordingly.
Slobodan: Ok, solved, thanks! I now see the tooltip all the time. It seems that the `tooltip` and `target` variables are never assigned.
CofounderGPT: Yes, you’re right. The problem is because the variables tooltip
and target
are not being assigned correctly. The binding in Svelte should be done with a lower-case ‘bind’, not uppercase. Here’s how to bind those variables:
<div class="tooltip" data-knowlo-tooltip bind:this={tooltip}>
And for the target, in your parent component where you’re using this tooltip, you could add:
<button data-knowlo-tooltip-id="my-tooltip" bind:this={target}>Hover me for tooltip</button>
<KnowloTooltip id="my-tooltip" {target} />
In your KnowloTooltip.svelte
file, you need to get target
from component’s props:
<script lang="ts">
export let target: HTMLElement;
// ... rest of your code
</script>
Note: Be sure to update your KnowloTooltip
class to use the target
prop instead of querying the document:
if (!this.target) return;
This conversation went on a bit longer, and it was fun, but I ended up debugging an AI-generated code in a framework I never used before. So I decided to take a break and learn a few things about Svelte first.
Svelte can generate a WebComponent. A good starting point was Svelte’s documentation, especially the Custom Elements API section.
The WebComponent code is not completely different from the code we had before. The most notable change is using the onMount
and beforeDestroy
lifecycle functions to replace the functionality of connectedCallback
and disconnectedCallback
in a web component.
We create a new web component custom element by using the <svelte:options customElement="knowlo-tooltip" />
on top of the Svelte file. In addition to that, we can bind the tooltip HTML element to the Svelte tooltip
variable by adding the following in the template:
<div class="tooltip" data-knowlo-tooltip bind:this={tooltip}>
Now we can access that element in the onMount
lifecycle function. However, to access the button itself, we’ll need to use the document.querySelector
function (or something similar to that).
However, even with the generated code, building a Svelte app from scratch can be tricky. The easiest way to get started is to run the npm create svelte@latest tooltip
command. This will offer you a few options, such as using Svelte or SvelteKit, using JavaScript or TypeScript, etc.
SvelteKit is a nice framework that can speed you up, especially if you are building a server-side rendering (SSR) app. But I decided to keep it simple, so I picked Svelte without Kit, TypeScript, and Vite.
The tooltip folder of our monorepo now looks similar to the following:
tooltip
' README.md
' index.html
' package.json
' public
? ' vite.svg
' src
? ' App.svelte
? ' app.css
? ' assets
? ? ' svelte.svg
? ' lib
? ? ' tooltip.svelte # A Web Component
? ? ' types.ts # Types for our Web Component
? ' main.ts
? ' vite-env.d.ts
' svelte.config.js # Svelte config file
' tsconfig.json
' tsconfig.node.json
' vite.config.ts. # Vite config file
Besides the configuration, the only important part is the tooltip.svelte
file. This file contains TypeScript, HTML, and SCSS. The SCSS can be extracted, but it took me at least 30 minutes to figure out how to do that without Kit, so I decided to keep it in the original file.
Anatomy of the Svelte Tooltip Web Component
The idea is to keep the component as simple as possible, so we’ll try to do the following:

So, once we initialize a component, we’ll need to get AWS Cognito User Pool credentials for an anonymous user, load the tooltip data from the API Gateway using these credentials, prepare the tooltip, and add JavaScript events (hover, click, etc.).
Then when a user hovers or clicks on the question mark icon, we need to show a tooltip and probably track some event. And if the user asks a question, we need to figure out which Helpdesk article to use for the answer and to contact the ChatGPT API to generate the response.
There are many things in the diagram above, and there are more than 8 hours to finish and test everything, so I’ll need to split this article and work into 2 or even 3 parts.
Let’s start with simple things first. To show and hide a tooltip, we can set the CSS display property to “block” or “none” with following the functions:
const show = () => {
if (tooltip) {
tooltip.style.display = 'block'
}
}
const hide = () => {
setTimeout(() => {
if (!mouseIsOverTooltip && !tooltipElementHasMouseOverOrFocus && tooltip) {
tooltip.style.display = 'none'
}
}, 10)
}
We still use the 10-millisecond timeout to allow users to move their mouse from the question mark icon (or any other element) to the tooltip itself.
We’ll use the following function to get temporary AWS Cognito Identity Pool credentials:
// Somewhere on top we import required npm modules
import { fromCognitoIdentityPool } from '@aws-sdk/credential-provider-cognito-identity'
import { CognitoIdentityClient } from '@aws-sdk/client-cognito-identity'
// Then we define a function in Svelte's script tag
const getCredentials = async (identityPoolId: string, region: string) => {
const cognitoidentity = new CognitoIdentityClient({
region: region, // Region from an environment variable
credentials : fromCognitoIdentityPool({
client : new CognitoIdentityClient({
region: region,
}),
identityPoolId: identityPoolId, // Cognito Identity Pool ID from an environment variable
}),
})
return await cognitoidentity.config.credentials()
}
This code uses the @aws-sdk/credential-provider-cognito-identity
and @aws-sdk/client-cognito-identity
npm modules and returnes temporary AWS access key ID, secret access key, and a session token.
These temporary credentials allow us to send a request to API Gateway and get a tooltip or an answer. We can do that without temporary credentials, but that would mean we need to manually implement the anonymous user tracking, which I want to avoid.
But how do we use these temporary credentials? We must sign the request with AWS Signature Version 4 or notorious AWS SigV4. Let’s ask CofounderGPT for help!
Slobodan: How do I use fetch to invoke api gateway using sig v4 from the frontend?
CofounderGPT: Using Fetch to invoke an API Gateway with AWS Signature Version 4 (SigV4) is a bit complex, because the SigV4 signing process is not straightforward and typically involves backend processes due to the need for sensitive AWS credentials.
However, you can use libraries such as AWS Amplify or AWS SDK for JavaScript in the Browser which handle SigV4 signing for you. The AWS SDK for JavaScript in the Browser also allows for direct integration with Amazon Cognito for user authentication and access control.
Here is a basic example using AWS SDK for JavaScript:
const apigClientFactory = require('aws-api-gateway-client').default;
const AWS = require('aws-sdk');
AWS.config.region = 'us-west-2';
AWS.config.credentials = new AWS.CognitoIdentityCredentials({
IdentityPoolId: 'us-west-2:12345678-1234-1234-1234-123456789012'
});
const apigClient = apigClientFactory.newClient({
region: 'us-west-2',
accessKey: AWS.config.credentials.accessKeyId,
secretKey: AWS.config.credentials.secretAccessKey,
sessionToken: AWS.config.credentials.sessionToken
});
var params = {};
var body = {};
var additionalParams = {};
apigClient.invokeApi(params, pathTemplate, method, additionalParams, body)
.then(function(result){
//This is where you would put a success callback
}).catch( function(result){
//This is where you would put an error callback
});
Please note that it’s not advisable to expose your AWS credentials on the client side as it poses a huge security risk. Therefore, it’s recommended to use temporary credentials with Amazon Cognito or some similar secure method.
As of my knowledge cutoff in September 2021, there’s no standard, safe way to perform SigV4 signing purely on the frontend using Fetch. If you find yourself needing to do this, it might be a good opportunity to reconsider your application’s architecture.
Ok, this sounds like “fun.” We’ll leave it for the next part (published early next week). Ok, this sounds like “fun.” We’ll leave it for the next part. Exposing the credentials in the frontend application is not smart, but we use temporary credentials that can do just one thing – execute an API, so it’s ok.
Scoreboard
Time spent today: 8h
Total time spent: 186h
Investment today: $0 USD
Total investment: $1,284.45 USD
Beta list subscribers: 91
Paying customers: 0
Revenue: $0
What’s Next?
The next step for CofounderGPT and I is to connect the tooltip with an API. After that, we need to show the analytics on the dashboard and add invitation codes to the signup process (we’ll need to roll this out in a controlled environment), and hopefully, we’ll onboard our first users after Lav’s vacation.
Comments are closed