0
\$\begingroup\$

I have created a Typewriter component that animates a span to display text for my portfolio website. The goal was to have an array of strings that are cycled through and an animation that types them out and deletes them one character at a time.

My first solution was agnostic to the size of each string. The animation would run for a fixed width (often causing the cursor to stray far from the text itself) so that did not appeal to me aesthetically. Furthermore, despite using a monospaced font the animation would not reveal exactly one char at a time like I wanted.

My next solution involved having unique animations for a set of string lengths and attempting to dynamically assign the animation to the span based on the characters in the string. This resulted in large blocks of repeated code to describe the animations and assign them with tailwindCSS. The font width issue persisted with this solution.

After posting on StackOverflow I realized I could simply update the text to display one char at a time and so arrived at the following code. This has given me exactly what I was hoping for visually.

I am new to the react/nextjs environment and would like to ensure I am following best practices and writing performant code. I would appreciate any thoughts. Thank you!

This is the effect created

Typewriter.tsx

"use client";
let titleLength: number;

const Typewriter = () => {
  const [title, setTitle] = useState<string>();
  const [typedText, setTypedText] = useState<string>();
  const title_picker = useRef(0);
  const char_counter = useRef(0);
  const pause_per_title = 200;
  const forward_type_speed = 200;
  const backward_type_speed = 100;

  useEffect(() => {
    let lastTitle: string;
    const changeTitle = () => {
      let timer: number;
      let newTitle: string;
      do {
        if (!lastTitle) {
          const index = Math.floor(Math.random() * titles.length);
          newTitle = titles[index];
        } else {
          const index = (titles.indexOf(lastTitle) + 1) % titles.length;
          newTitle = titles[index];
        }
      } while (newTitle === lastTitle);
      titleLength = newTitle.length;
      timer = titleLength * (forward_type_speed + backward_type_speed) + pause_per_title;
      setTitle(newTitle);
      lastTitle = newTitle;
      title_picker.current = window.setTimeout(changeTitle, timer);
    };
    changeTitle();
    return () => {
      window.clearTimeout(title_picker.current);
    };
  }, []);

  useEffect(() => {
    let charsTyped: number = 0;
    let typingForward: boolean = true;
    setTypedText("");
    const typeText = () => {
      if (title) {
        if (typingForward && title.length === charsTyped) {
          typingForward = false;
        }
        if (typingForward && charsTyped < title.length) {
          charsTyped = charsTyped + 1;
          setTypedText(title.slice(0, charsTyped));
          char_counter.current = window.setTimeout(typeText, forward_type_speed);
        } else if (!typingForward && charsTyped != 0) {
          charsTyped = charsTyped - 1;
          setTypedText(title.slice(0, charsTyped));
          char_counter.current = window.setTimeout(typeText, backward_type_speed);
        }
      }
    };
    typeText();
    return () => {
      window.clearTimeout(char_counter.current);
    };
  }, [title]);

  return (
    <>
      <span className="text-secondary font-semibold font-mono overflow-clip border-r-2 animate-blink">
        {typedText}
      </span>
    </>
  );
};

export default Typewriter;

tailwind.config.js

...
    extend: {
      keyframes: {
        blink: {
          from: { "border-right-color": "transparent" },
          to: { "border-right-color": "secondary" },
        },
      },
      animation: {
        blink: "blink 0.4s steps(2) infinite",
      },
...
\$\endgroup\$

0

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Browse other questions tagged or ask your own question.