Adding a little “extra” to a blog often makes it feel more alive. For my search page, I decided to implement a typewriter effect that cycles through my post titles as placeholder text in the search input. It’s a small detail, but it gives users a hint of what they can search for while adding a dynamic feel.

Here’s how I built it.

The Goal

  1. Fetch all blog post titles from the search index.
  2. Randomly select a title and “type” it out in the search bar’s placeholder.
  3. Include a blinking cursor (|).
  4. Delete the text after a short pause and start again with a new title.
  5. Stop the effect if the user starts typing manually.

Step 1: Getting the Data

Since I’m using the PaperMod theme with Hugo, my search index is already generated as a JSON file (index.json). I hooked into the existing search script (fastsearch.js) where the search index is loaded.

window.onload = function () {
    let xhr = new XMLHttpRequest();
    xhr.onreadystatechange = function () {
        if (xhr.readyState === 4 && xhr.status === 200) {
            let data = JSON.parse(xhr.responseText);
            if (data) {
                // Extract titles and start the typewriter
                const titles = data.map(item => item.title);
                typewriterPlaceholder(titles);
            }
        }
    };
    xhr.open("GET", "../index.json", true);
    xhr.send();
};

Step 2: The Typewriter Logic

The core logic lives in typewriterPlaceholder(titles). I needed a few state variables to keep track of where we are:

  • currentTitleIndex: Which title are we currently typing?
  • charIndex: How many characters have been typed?
  • isDeleting: Are we typing or backspacing?
  • showCursor: For that classic blinking effect.

The Blinking Cursor

I used a separate setInterval for the cursor to ensure it blinks at a consistent rate, regardless of how fast the text is being typed.

setInterval(() => {
    showCursor = !showCursor;
    if (sInput && sInput.value.length === 0) {
        sInput.placeholder = currentText + (showCursor ? "|" : " ");
    }
}, 500);

The Main Typing Loop

The type() function handles the actual string manipulation. I use setTimeout recursively to control the speed.

function type() {
    // If the user has typed something, pause the effect
    if (sInput.value.length > 0) {
        currentText = "";
        sInput.placeholder = "";
        setTimeout(type, 1000);
        return;
    }

    const fullTitle = titles[currentTitleIndex];
    
    if (isDeleting) {
        currentText = fullTitle.substring(0, charIndex--);
    } else {
        currentText = fullTitle.substring(0, charIndex++);
    }

    sInput.placeholder = currentText + (showCursor ? "|" : " ");

    // Logic for pausing at the end and switching titles
    if (!isDeleting && charIndex > fullTitle.length) {
        isDeleting = true;
        setTimeout(type, 2200); // Pause when title is fully typed
    } else if (isDeleting && charIndex < 0) {
        isDeleting = false;
        currentTitleIndex = Math.floor(Math.random() * titles.length);
        charIndex = 0;
        setTimeout(type, 500);
    } else {
        let speed = isDeleting ? 25 : 30;
        setTimeout(type, speed);
    }
}

Step 3: Handling User Interaction

We don’t want the placeholder to “fight” with the user. If they click into the search bar and start typing, the typewriter should yield.

sInput.addEventListener('input', function() {
    if (this.value.length === 0) {
        // Reset if they clear the input
        currentText = "";
        charIndex = 0;
        isDeleting = false;
        currentTitleIndex = Math.floor(Math.random() * titles.length);
    }
});

Conclusion

That’s it! It’s a purely vanilla JavaScript solution that doesn’t require any heavy libraries. It makes use of the existing index.json search data, so it’s efficient and always shows relevant suggestions.

You can check it out in action on the search page!