Hello and welcome to my first article written in 2025! Today I’m so proud of myself of making a commit of something really great something really useful and definitely productive and not a waste of my time, which is this loading text animation.
It’s beautiful isn’t it? 🥹
Looking back it was pretty amazing how I can pull off such a small yet daunting task. It Anyways, if you want to skip to the tutorial, here it is…
How to make the composable
First if you haven’t yet, create a file for your composables. You can put it in a composables folder in src
or in the root of the project if you use Nuxt. You want to create a function starting with use
:
export function useLoadingText() {
// what do we write here lol
}
Here we start by defining some parameters in an object to reduce possible ambiguity:
interface LoadingTextParams {
isLoading: boolean;
text: string;
loadingText: string;
}
export function useLoadingText(params: LoadingTextParams) {
// ...
}
but, we are using Vue after all and we might pass some live refs in there, so we use MaybeRefOrGetter
and use the toRef
utility provided by Vue to create the final reactive loading text side effects… except for the isLoading
param because we want it to be always a Vue ref.
import { Ref, toValue, type MaybeRefOrGetter } from "vue";
interface LoadingTextParams {
isLoading: Ref<boolean>;
text: MaybeRefOrGetter<string>;
loadingText: MaybeRefOrGetter<string>;
}
export function useLoadingText(params: LoadingTextParams) {
const ploadingText = toValue(params.loadingText);
}
and then we want to watch the isLoading
ref to react when the isLoading
state changes, then use that to return a reactive loadingText
that can be used as a loading indicator:
// ... rest of code
export function useLoadingText(params: LoadingTextParams) {
const pLoadingText = toValue(params.loadingText);
const finalText = ref(params.text);
watch(params.isLoading, (newIsLoading) => {
// todo: add dots on specified times
finalText.value = newIsLoading.value ? pLoadingText.value : params.text;
});
return { loadingText: finalText };
}
Next, we will use setTimeout
to append dots to the finalText
:
// .. rest of your useLoadingText code
watch(params.isLoading, (newIsLoading) => {
setInterval(() => {
finalText.value = params.loadingText ".";
}, 500); // add dots every 500ms
finalText.value = newIsLoading.value ? pLoadingText.value : pText.value;
});
we will now store the timeout ID returned by setInterval
and pass it to clearInterval
when isLoading
is false to stop the dots from adding infinitely.
// ... rest of your useLoadingText code
let intervalID: NodeJS.Timeout | undefined;
watch(params.isLoading, (newIsLoading) => {
if (newIsLoading) {
intervalID = setInterval(() => {
finalText.value += ".";
}, 500); // add dots every 500ms
return;
}
clearInterval(intervalID);
intervalID = undefined;
finalText.value = newIsLoading.value ? pLoadingText.value : pText.value;
});
But when doing this we notice that the finalText
will be appended infinitely, and only after stopping the isLoading the finalText
comes back to its initial state. So for the solution, we will store how many dots have been appended, and when it’s already three, we set it back to the loadingText
without the dots.
To counter this, we will store the amount of dots that have been appended, and when it reaches three, we will reset it back to 0. we will also set it back to 0 when the isLoadingState is false.
// .. rest of your useLoadingText code
const repeatAmount = ref(0);
watch(params.isLoading, (newIsLoading) => {
if (newIsLoading) {
intervalID = setInterval(() => {
finalText.value += ".".repeat(repeatAmount.value);
repeatAmount.value += 1;
if (repeatAmount.value > 3) repeatAmount.value = 1;
}, 500); // add dots every 500ms
return;
}
clearInterval(intervalID);
intervalID = undefined;
finalText.value = newIsLoading.value ? pLoadingText.value : pText.value;
});
Now that we have all the pieces, we can put it all together:
import { watch, ref, toValue, type Ref, type MaybeRefOrGetter } from "vue";
interface LoadingTextParams {
isLoading: Ref<boolean>;
text: MaybeRefOrGetter<string>;
loadingText: MaybeRefOrGetter<string>;
}
export function useLoadingText(params: LoadingTextParams) {
const pLoadingText = toValue(params.loadingText);
const finalText = ref(params.text);
const repeatAmount = ref(0);
let intervalID: NodeJS.Timeout | undefined;
watch(params.isLoading, (newIsLoading) => {
finalText.value = newIsLoading ? pLoadingText : params.text;
if (newIsLoading) {
return (intervalID = setInterval(() => {
finalText.value = pLoadingText + ".".repeat(repeatAmount.value);
repeatAmount.value += 1;
if (repeatAmount.value > 3) repeatAmount.value = 1;
}, 500)); // add dots every 500ms
}
repeatAmount.value = 0;
clearInterval(intervalID);
intervalID = undefined;
});
return { loadingText: finalText };
}
Test it out
Now we have put together a simple composable to render a loading text! You can test it in a vue page like so:
<script setup lang="ts">
import { ref } from "vue";
import { useLoadingText } from "@/composables/";
const isLoading = ref(false);
const { loadingText } = useLoadingText({
isLoading,
text: "Not loading yet",
loadingText: "loading",
});
</script>
<template>
<section class="page">
<h1>{{ loadingText }}</h1>
<div class="container">
<button @click="isLoading = true" :disabled="isLoading">
Let's load
</button>
<button @click="isLoading = false" :disabled="!isLoading">
Stop loading
</button>
</div>
</section>
</template>
<style scoped>
.page {
min-height: 100vh;
display: grid;
place-items: center;
align-content: center;
}
.container {
display: flex;
gap: 1rem;
}
</style>
You can see a live demo right here:
have fun using it in your projects!