DEV Community

Cover image for Designing a Typewriter React Component
Purbayan Chowdhury
Purbayan Chowdhury

Posted on • Edited on

Designing a Typewriter React Component

We are pretty much familiar with the Typewriter effect, although we might not be acquainted with a Typewriter. In words, the typewriter effect is the gradual revealing of the words as if it is being typed infront of our eyes with sound of a typewriter key pressing. A popular Typewriter Animation in Web usually involves slowling revealing of the text with a blinking cursor and slowling erasing of the text with a pause.

Though in our today's exercise, we will be implementing a typewriter effect where a list of words are being typed on the screen with a blinking caret or cursor. After each word is being typed, it's also erased after a little pause to erase slowly one letter at a time and finally typed in for the next word.

Getting Started

We won't require no extra libraries except the ones installed by create-react-app template.

Typewriter.jsx

import React from 'react';

function Typewriter({ text, ...otherProps }) {
    return (
        <div className="Typewriter" {...otherProps}>
            <span className="Typewriter__text">{text}</span>
            <span className="Typewriter__cursor">|</span>
        </div>
    );
}

export default Typewriter;

Enter fullscreen mode Exit fullscreen mode

A classic functional component that has text (string) prop for content and two child components, i.e., typewriter_text and typewriter_cursor.

Implementation

Blinking caret

To design the blinking caret, we will need css into action.

Typewriter.css

.Typewriter__text {
    display: inline-block;
}

.Typewriter__cursor {
    display: inline-block;
    color: currentColor;
    animation: blink 1s ease-in-out 0s infinite alternate;
}

@keyframes blink {
    from {
        opacity: 1;
    }
    to {
        opacity: 0;
    }
}
Enter fullscreen mode Exit fullscreen mode

CSS3 Animations is used for blinking and both child components are made inline-block for making them side by side. Add a import in Typewriter jsx after React import

import React from 'react';
import './Typewriter.css';

...
Enter fullscreen mode Exit fullscreen mode

Typing Effect

We will use two React Hooks namely useState and useEffect for this purpose.

function Typewriter({ text, speed, ...otherProps }) {
    const [currentText, setCurrentText] = React.useState('');
    const [__timeout, set__Timeout] = React.useState(null);

    React.useEffect(() => {
        startTyping();

        return () => {
            __timeout && clearTimeout(__timeout);
        };
    }, []);

    React.useEffect(() => {
        let rawText = text;
        if (currentText.length < rawText.length) {
            set__Timeout(setTimeout(type, speed));
        }
        return () => {
            __timeout && clearTimeout(__timeout);
        };
    }, [currentText]);

    function startTyping() {
        set__Timeout(
            setTimeout(() => {
                type();
            }, speed)
        );
    }

    function type() {
        let rawText = text;

        if (currentText.length < rawText.length) {
            let displayText = rawText.substr(0, currentText.length + 1);
            setCurrentText(displayText);
        }
    }

    return (
        <div className="Typewriter" {...otherProps}>
            <span className="Typewriter__text">{currentText}</span>
            <span className="Typewriter__cursor">|</span>
        </div>
    );
}
Enter fullscreen mode Exit fullscreen mode

The function startTyping initiates the first call for text change. The function type updates the current text while on every update of currentText, type function is called after every speed (which is passed as a prop) milliseconds.

Erasing Effect

We have already implemented the typing effect, and for erasing effect, we need a flag to know whether we are typing or erasing. Thereby, we can create a cycle of typing to erasing and vice-versa.

function Typewriter({ text, speed, eraseSpeed, typingDelay, eraseDelay, ...otherProps }) {
    ...
    const [isTyping, setIsTyping] = React.useState(true);

    ...

    React.useEffect(() => {
        let rawText = text;
        if (isTyping) {
            if (currentText.length < rawText.length) {
                set__Timeout(setTimeout(type, speed));
            } else {
                setIsTyping(false);
                set__Timeout(setTimeout(erase, eraseDelay));
            }
        } else {
            if (currentText.length === 0) {
                setIsTyping(true);
                setTimeout(startTyping, typingDelay);
            } else {
                set__Timeout(setTimeout(erase, eraseSpeed));
            }
        }
        return () => {
            __timeout && clearTimeout(__timeout);
        };
    }, [currentText]);

    ...

    function erase() {
        if (currentText.length !== 0) {
            let displayText = currentText.substr(-currentText.length, currentText.length - 1);
            setCurrentText(displayText);
        }
    }

    ...

}
Enter fullscreen mode Exit fullscreen mode

Added an erase function for diminishing effect and a state variable isTyping for erasing or typing switch. Updated the useEffect on currentText for startTyping when currentText.length is zero with typingDelay (added to the props) seconds and switch to typing, else erase is called after every eraseSpeed (added to the props) milliseconds. For typing mode, added switch to erasing after erasingDelay when currentText.length reaches full length.

Enabling Array of Text

We need to add an index for the array and function to handle array or string for text prop.

function Typewriter({ text, speed, eraseSpeed, typingDelay, eraseDelay, ...otherProps }) {
    ...
    const [currentIndex, setCurrentIndex] = React.useState(0);

    ...
    React.useEffect(() => {
        let rawText = getRawText()[currentIndex];
        if (isTyping) {
        ...
        } else {
            if (currentText.length === 0) {
                const textArray = getRawText();
                let index = currentIndex + 1 === textArray.length ? 0 : currentIndex + 1;
                if (index === currentIndex) {
                    setIsTyping(true);
                    setTimeout(startTyping, typingDelay);
                } else {
                    setTimeout(() => setCurrentIndex(index), typingDelay);
                } 
            }
            else {
                set__Timeout(setTimeout(erase, eraseSpeed));
            }
        }
        ...
    }, [currentText]);   


    React.useEffect(() => {
        if (!isTyping) {
            setIsTyping(true);
            startTyping();
        }
        return () => {
            __timeout && clearTimeout(__timeout);
        };
    }, [currentIndex]);

    function getRawText() {
        return typeof text === 'string' ? [text] : [...text];
    }

    ...
    function type() {
        let rawText = getRawText()[currentIndex];
        ...
    }

    function erase() {
        let index = currentIndex;
        if (currentText.length !== 0) {
            let displayText = currentText.substr(-currentText.length, currentText.length - 1);
            setCurrentText(displayText);
        } else {
            const textArray = getRawText();
            index = index + 1 === textArray.length ? 0 : index + 1;
            setCurrentIndex(index);
        }
    }

    ...
}

Enter fullscreen mode Exit fullscreen mode

Added getRawText function to handle string or array at the same time from text prop and added state variable currentIndex for array index. Updated useEffect for currentText, in erasing mode, to switch to next string in array and start typing. Added useEffect for currentIndex, to setTyping true and startTyping.

Alternate cursor

...
function Typewriter({ text, speed, eraseSpeed, cursor, typingDelay, eraseDelay, ...otherProps }) {
    ...
    return (
        <div className="Typewriter" {...otherProps}>
            <span className="Typewriter__text">{currentText}</span>
            <span className="Typewriter__cursor">{cursor}</span>
        </div>
    );
}
Enter fullscreen mode Exit fullscreen mode

Added cursor to the prop and added the same in return section of the function

Adding PropTypes and default Props

Added import for proptypes

import React from 'react';
import PropTypes from 'prop-types';
import './Typewriter.css';
...
Enter fullscreen mode Exit fullscreen mode

Added defaultProps for speed, eraseSpeed, typingDelay and eraseDelay

Typewriter.propTypes = {
    speed: PropTypes.number.isRequired,
    eraseSpeed: PropTypes.number.isRequired,
    typingDelay: PropTypes.number.isRequired,
    eraseDelay: PropTypes.number.isRequired,
    cursor: PropTypes.string,
    text: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.string), PropTypes.string]).isRequired,
};

Typewriter.defaultProps = {
    speed: 500,
    eraseSpeed: 400,
    typingDelay: 2500,
    eraseDelay: 5000,
};
Enter fullscreen mode Exit fullscreen mode

Final Code

Final Code for Typewriter.jsx

https://github.com/shivishbrahma/nuclear-reactor/blob/main/src/Typewriter/Typewriter.jsx

Use withtin App Component

import './App.css';
import Typewriter from './Typewriter';

function App() {
    return <div className="App">
        <Typewriter text={["Typewriter Effect", "A React Component"]}/>
    </div>;
}

export default App;
Enter fullscreen mode Exit fullscreen mode

Preview

Preview of Typewriter

References

Top comments (0)