"A typewriter forces you to keep going, to march forward." - James McBride
I'm taking a break this week from writing about writing. Instead, I will demonstrate how to create a Typewriter Component in Vue.js.
Here's a preview:
Template
The template is quite simple. To create the typewriter effect, you need an element for static text and an element for changing text. This component contains two span
tags encapsulated in a div
. I also tried a variant of a p
tag encapsulating the span
tag of the changing text.
<div class="pl-10">
<span class="text-4xl text-black">
{{ title }}
</span>
<span class="text-4xl text-bold text-red-500">
{{ displayText.join("") }}
</span>
</div>
Styles
For simplicity, I've used Tailwind CSS for styling.
Script
Props & Computed Values
This component takes in 4 props: title, speed, deleteSpeed, and words. The title
prop is the static text. The speed
prop is the typing speed, and the deleteSpeed
prop is the delete speed. The words
prop is an array of changing words. While computed values are not needed in this simple example, I pondered if there might be a use case where certain conditions may require internally changing the values (such as having a super slow delete speed for words that match a certain value).
Data
There's only 3 data values - a displayText
array, which keeps track of which values to display, the currentWord
being "typed", and the index of the current word from the words
array.
Methods
Start
This begins the typing sequence, setting the currentWord
and using a setTimeout
interval for a delay before calling the type function to continue the typing sequence.
Type
This method contains all the logic to determine which word is being typed, whether to type or delete, or to change to the next word. Take a look below.
// if typing...
if (this.currentWord.length > 0) {
this.displayText.push(this.currentWord.shift());
// if done typing, then delete
} else if (this.currentWord.length === 0 &&
this.displayText.length > 0) {
this.timeoutSpeed = this.DELETE_SPEED;
this.displayText.pop();
// if done typing & deleting
} else if (
this.currentWord.length === 0 &&
this.displayText.length === 0
) {
// change words
if (this.wordIdx < this.words.length) {
this.currentWord = this.words[this.wordIdx].split("");
this.wordIdx++;
this.timeoutSpeed = this.TYPE_SPEED;
this.displayText.push(this.currentWord.shift());
} else {
// reset
this.wordIdx = 0;
this.currentWord = this.words[this.wordIdx].split("");
this.displayText.push(this.currentWord.shift());
}
}
setTimeout(this.type, this.timeoutSpeed);
Mounted Lifecycle
When the component is mounted, it calls the start()
method to begin the typing sequence.
Here's the final Codepen code:
And a Github Gist for the Single Page Component:
Code reviews welcome. Let me know if I can do something better.
Update [16 Oct 2020]: Take a look at Theo's Comment for ways to improve this component!
Just some fixes and two features:
- Blink cursor.
- Add interval between words/phrases cycle.
<template>
<span>
{{ displayText.join('') }}
<span class="cursor">|</span>
</span>
</template>
<script>
export default {
props: {
speed: {
type: Number,
default: 100,
},
deleteSpeed: {
type: Number,
default: 30,
},
nextWordInterval: {
type: Number,
default: 1200
},
words: {
type: Array,
default: [],
},
},
data() {
return {
displayText: [],
currentWord: '',
wordIdx: 0,
timeoutSpeed: null,
isWaitingNextWord: false,
}
},
mounted() {
this.start()
},
methods: {
start() {
if (this.words && this.words.length > 0) {
this.currentWord = this.words[this.wordIdx].split('')
this.timeoutSpeed = this.speed
this.animate = setTimeout(this.type, this.timeoutSpeed)
}
},
type() {
// if typing...
if (this.currentWord.length > 0) {
this.displayText.push(this.currentWord.shift())
// if done typing, wait for a while
} else if (!this.isWaitingNextWord && this.currentWord.length === 0 && this.displayText.length === this.words[this.wordIdx].length) {
this.timeoutSpeed = this.nextWordInterval
this.isWaitingNextWord = true
// if done typing, then delete
} else if (this.currentWord.length === 0 && this.displayText.length > 0) {
this.timeoutSpeed = this.deleteSpeed
this.displayText.pop()
// if done typing & deleting
} else if (this.currentWord.length === 0 && this.displayText.length === 0) {
// change words
if (this.wordIdx < (this.words.length - 1)) {
this.wordIdx++
} else {
// reset
this.wordIdx = 0
}
this.timeoutSpeed = this.speed
this.isWaitingNextWord = false
this.currentWord = this.words[this.wordIdx].split('')
this.displayText.push(this.currentWord.shift())
}
setTimeout(this.type, this.timeoutSpeed)
},
},
}
</script>
<style lang="scss" scoped>
@keyframes blink-animation {
to {
visibility: hidden;
}
}
.cursor {
display: inline-block;
margin-left: -3px;
animation: blink-animation 1s steps(2, start) infinite;
}
</style>
Top comments (3)
Just some fixes and two features:
This was a great addition, I'd also like to contribute some changes I've made.
I put all the main functionality of the loop into individual functions for better state management. I personally loved the addition of the cursor since it was exactly what I was looking for, but I had an issue with the look of the cursor. I wanted it to be exact to what's seen when you actually type. Ex: Typing, cursor is static, stop typing, cursor blinks.
Thanks Theo! I've included your reply above as part of an update!