How to Add a Looping Typewriter Subtitle to Your Astro Banner
This guide walks through implementing a typewriter effect for a banner subtitle in an Astro + Yukina setup. The effect progressively reveals the subtitle, holds the fully typed text for three seconds, then restarts in a smooth loop. The subtitle remains horizontally centered beneath the main title.
Prerequisites
- Astro project using the shared banner component pattern (
src/components/Banner.astro) - Tailwind utilities available (the snippets rely on Tailwind classes)
- Access to your layout that renders the banner (
src/layouts/MainLayout.astro) and the routed page that should enable the effect (e.g.,src/pages/[...page].astro)
Step 1: Add a toggle prop to the Banner component
Introduce a typewriterSubtitle?: boolean prop so the effect can be switched on where needed.
// src/components/Banner.astro
export interface Props {
title?: string;
subTitle?: string;
bannerImage?: string;
slug?: string;
typewriterSubtitle?: boolean;
}
const { title, subTitle, bannerImage, slug, typewriterSubtitle } = Astro.props;
const subtitleText = subTitle ?? YukinaConfig.subTitle;
Use the prop to conditionally apply a typewriter class and set CSS variables for the character count:
<h2
class:list={["subtitle", typewriterSubtitle && "subtitle-typewriter"]}
style={
typewriterSubtitle
? `--typewriter-steps: ${subtitleText.length}; --typewriter-width: ${subtitleText.length}ch;`
: undefined
}
>
{subtitleText}
</h2>
Step 2: Add the typewriter styles and timing
Append the animation styles to the banner stylesheet. The 7-second loop types for roughly 4 seconds and holds the completed text for about 3 seconds before restarting. The subtitle is centered with a slight margin from the title.
/* src/components/Banner.astro */
.subtitle {
@apply mt-3 text-center text-xl text-[var(--subtitle-color)] drop-shadow-md lg:mt-4 lg:text-3xl;
font-family: var(--subtitle-font);
}
.subtitle-typewriter {
@apply mx-auto inline-block overflow-hidden whitespace-nowrap;
border-right: 2px solid var(--subtitle-color);
width: 0;
animation:
typing 7s steps(var(--typewriter-steps), end) infinite,
blink-caret 0.75s step-end infinite;
}
@keyframes typing {
0% {
width: 0;
}
58% {
width: var(--typewriter-width, 100%);
}
100% {
width: var(--typewriter-width, 100%);
}
}
@keyframes blink-caret {
from,
to {
border-color: transparent;
}
50% {
border-color: var(--subtitle-color);
}
}
Step 3: Plumb the prop through the main layout
Expose the toggle on your layout so any page can enable the effect when rendering the banner.
// src/layouts/MainLayout.astro
export interface Props {
title?: string;
subTitle?: string;
bannerImage?: string;
slug?: string;
typewriterSubtitle?: boolean;
}
<Banner
title={props.title}
subTitle={props.subTitle}
bannerImage={props.bannerImage}
slug={props.slug}
typewriterSubtitle={props.typewriterSubtitle}
/>;
Step 4: Turn it on for the desired route (example: homepage)
If your homepage is the first page of a paginated list, you can enable the typewriter only there by passing the prop when currentPage === 1.
// src/pages/[...page].astro
<Main typewriterSubtitle={page.currentPage === 1}>
<!-- page content -->
</Main>
You can also hardcode typewriterSubtitle={true} on any specific page or layout slot where the effect is desired.
Step 5: Verify the behavior
- Run
pnpm devand open the homepage. - Confirm the subtitle types from left to right, pauses for roughly three seconds after completion, then restarts.
- Check that the subtitle is horizontally centered and sits slightly below the main title.
Optional adjustments
- Typing pace and pause: Change
typing 7s ...to alter the full cycle duration; adjust the58%keyframe to control how quickly the text finishes before the pause. - Caret styling: Modify
border-rightor theblink-caretkeyframe for color and blink speed. - Scope: Pass
typewriterSubtitleonly on routes where the effect fits the page tone.