Create Multi‑Line Typing Animation in Vue 3 Without DOM Jank
This article walks through building a realistic typing‑effect dialog box using CSS keyframe animation, JavaScript timers, requestAnimationFrame optimization, and a Vue 3 directive that splits long text into lines for smooth multi‑line display while minimizing DOM updates.
Introduction
After the popularity of DeepSeek, our company received an AI project that required a human‑machine conversation dialog box. The backend pushes messages via a WebSocket connection to the frontend.
Requirement
The product expects the dialogue to show a realistic "typing effect" similar to commercial AI products.
Implementation
1. CSS Animation
@keyframes typing {
from { width: 0; }
to { width: 100%; }
}
.typing-effect {
overflow: hidden;
white-space: nowrap;
animation: typing 6s steps(50, end);
}HTML usage:
<div class="typing-effect">日照香炉生紫烟,遥看瀑布挂前川。飞流直下三千尺,疑是银河落九天。</div>Problem
The CSS method cannot handle multi‑line text; overflow is cut off when the content exceeds one line.
2. JavaScript Timer
function typeWriter(elementId, message, speed) {
const element = document.getElementById(elementId);
let i = 0;
const interval = setInterval(() => {
if (i < message.length) {
element.textContent += message.charAt(i);
i++;
} else {
clearInterval(interval);
}
}, speed);
}
// Example
typeWriter('typewriter', '日照香炉生紫烟,遥看瀑布挂前川。飞流直下三千尺,疑是银河落九天。', 100);This solves the multi‑line issue but may cause stutter due to timer‑based animation.
3. Optimized Animation with requestAnimationFrame
function typeWriter(elementId, message, speed) {
const element = document.getElementById(elementId);
let i = 0;
let startTime = null;
function animate(currentTime) {
if (!startTime) startTime = currentTime;
if (currentTime - startTime >= i * speed) {
if (i < message.length) {
element.textContent += message.charAt(i);
i++;
requestAnimationFrame(animate);
}
} else {
requestAnimationFrame(animate);
}
}
requestAnimationFrame(animate);
}
// Example
typeWriter('typewriter', '日照香炉生紫烟,遥看瀑布挂前川。飞流直下三千尺,疑是银河落九天。', 100);Although smoother, frequent DOM updates still affect performance for long dialogues.
4. Back to CSS – Multi‑Line Solution
To avoid repeated DOM changes, we split the text into lines based on the container width and animate each line separately.
function calculateCharactersPerLine(divElement, fontSize, text) {
const canvas = document.createElement('canvas');
const context = canvas.getContext('2d');
context.font = `${fontSize}px sans-serif`;
const metrics = context.measureText(text);
const textWidth = metrics.width;
const divWidth = divElement.offsetWidth;
const charactersPerLine = Math.floor(divWidth / textWidth) * text.length;
if (charactersPerLine === 0) {
return calculateCharactersPerLine(divElement, fontSize, text.slice(0, -1));
}
return charactersPerLine;
}
function splitIntoChunks(str, chunkSize) {
const regexPattern = new RegExp(`.{1,${chunkSize}}`, 'g');
return str.match(regexPattern);
}Using these helpers, we create a Vue 3 directive that generates a series of div elements, each animated with a width‑expansion keyframe.
export function typewriter(app) {
app.directive('typewriter', (el, binding) => {
if (binding.oldValue) return;
renderText(el, binding);
});
}
function renderText(el, binding) {
const arg = binding.arg || 1;
const style = document.createElement('style');
style.textContent = `
@keyframes width {
0% { width: 0; }
100% { width: 100%; }
}
`;
document.head.appendChild(style);
const textLen = calculateCharactersPerLine(el, 16, binding.value);
const divList = splitIntoChunks(binding.value, textLen);
divList.forEach((row, index) => {
const oDiv = document.createElement('div');
oDiv.innerText = row;
oDiv.style.cssText = `
position: relative;
overflow: hidden;
width: 0;
white-space: nowrap;
animation: width ${arg}s steps(50) forwards;
animation-delay: ${index * arg}s;
`;
el.appendChild(oDiv);
});
} <template>
<div v-typewriter:[2]="`回环(文)诗、剥皮诗、离合诗、宝塔诗、字谜诗、辘轳诗、八音歌诗、藏头诗、打油诗、诙谐诗、集句诗、联句诗、百年诗、嵌字句首诗、绝弦体诗、神智体诗等40多种。这些杂体诗各有特点,虽然均有游戏色彩,但有些则具有一定的思想性和艺术性,所以深受人们的喜爱`"></div>
</template>Conclusion
Many developers over‑engineer simple problems, but any method that meets the functional requirement is acceptable. If you face a similar need in your project, which approach would you choose? Feel free to discuss in the comments.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Rare Earth Juejin Tech Community
Juejin, a tech community that helps developers grow.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
