26 / 36
All 36 in 36 Tailwind CSS Button Hover Effects
- 01Tailwind Magnetic Ripple Attraction Button Hover Effect
- 02Tailwind Cyberpunk Holographic Glitch Grid Button
- 03Tailwind Frosted Glassmorphism Shimmer Glow Button
- 04Tailwind Organic Blob Morph Button Hover Effect
- 05Tailwind Dual-Line Intersection Border Draw Button
- 06Tailwind Isometric Tactile 3D Depress Button
- 07Tailwind Infinite Sliding Kinetic Marquee Button
- 08Tailwind Chroma Conic Gradient Border Spin Button
- 09Tailwind Minimalist Border & Outline Drawing Hover Effects
- 10Tailwind Modern 3D Skeuomorphic Button Press Effects
- 11Tailwind Neo-Brutalist & Cyberpunk Button Glow Effects
- 12Tailwind Sliding Background Color Wipe Button Effects
- 13Tailwind Liquid Fill & Organic Morphing Button Effects
- 14Tailwind Kinetic Text Flip & Icon Reveal Button Effects
- 15Tailwind Liquid Wave Fill Button Hover Effect
- 16Tailwind Magnetic Border Drift Button
- 17Tailwind Glitch Slice Button Hover Effect
- 18Tailwind Neon Pulse Glow Button Hover Effect
- 19Tailwind Split Reveal Curtain Button Effect
- 20Tailwind Diagonal Shutter Sweep Button Effect
- 21Tailwind Elastic Lift Spring Button Hover Effect
- 22Tailwind Center Underline Draw Link Button Effects
- 23Tailwind 3D Flip Card Button Hover Effect
- 24Tailwind Particle Burst Confetti Button Effect
- 25Tailwind Slide Doors Closing Panel Button Effect
- 26Tailwind Typewriter Retype Terminal Button Effect
- 27Tailwind Hard Shadow Shift Flat Button Effect
- 28Tailwind Material Ripple Wave Button Effect
- 29Tailwind Rainbow Gradient Border Spin Button
- 30Tailwind Morph Shape Silhouette Button Effects
- 31Tailwind Icon Slide In Directional Button Effect
- 32Tailwind Cursor Spotlight Glow Button
- 33Tailwind Stagger Fill Equaliser Button Effect
- 34Tailwind Sunburst Expand Radiant Button Effect
- 35Tailwind 3D Press Plunger Key Button Effect
- 36Tailwind Add-to-Cart Liquid Glass Ripple Button
Tailwind Typewriter Retype Terminal Button Effect
A retro-terminal CTA where, on hover, the label retypes character by character with a blinking caret — nostalgic and techy for dev tools and docs. The ch unit and the steps() count are the whole effect, and both are now visible in the markup where the character count lives.
Untitled UI Pro: 5,000+ React + Tailwind CSS v4 components · from $349 one-time
Untitled UI React · Affiliate link
The code
<div class="tbh-26-group w-full min-h-svh flex flex-wrap">
<section class="tbh-26a flex-[1_1_340px] min-w-[300px] grid place-items-center px-6 py-14 font-tbh26-mono bg-tbh26a-stage" aria-label="Typewriter retype button">
<button type="button" class="group/btn cursor-pointer rounded-md border border-tbh26a-term px-7 py-4 text-base font-bold text-tbh26a-term bg-tbh26a-panel shadow-[0_0_20px_-8px_var(--color-tbh26a-term)] focus-visible:outline-3 focus-visible:outline-tbh26a-term focus-visible:outline-offset-4">
<span class="inline-block overflow-hidden whitespace-nowrap align-bottom w-[12ch] border-r-2 border-tbh26a-term animate-tbh26a-blink group-hover/btn:[animation:var(--animate-tbh26a-type),var(--animate-tbh26a-blink)] group-focus-visible/btn:[animation:var(--animate-tbh26a-type),var(--animate-tbh26a-blink)] motion-reduce:animate-none motion-reduce:w-[12ch]">$ deploy_now</span>
</button>
</section>
<section class="tbh-26b flex-[1_1_340px] min-w-[300px] grid place-items-center px-6 py-14 font-tbh26-mono bg-tbh26b-stage" aria-label="Prompt caret button">
<button type="button" class="group/btn inline-flex items-center gap-0.5 cursor-pointer rounded-md border border-tbh26b-term px-[30px] py-4 text-base font-bold text-tbh26b-term bg-tbh26b-panel focus-visible:outline-3 focus-visible:outline-tbh26b-term focus-visible:outline-offset-4">
<span class="overflow-hidden whitespace-nowrap max-w-[9ch] transition-[max-width] duration-700 ease-[steps(9)] group-hover/btn:max-w-0 group-focus-visible/btn:max-w-0 motion-reduce:transition-none motion-reduce:max-w-[9ch]">run build</span>
<span aria-hidden="true" class="w-[9px] h-[1.1em] bg-tbh26b-term animate-tbh26b-blink group-hover/btn:animate-tbh26b-blinkfast group-focus-visible/btn:animate-tbh26b-blinkfast motion-reduce:animate-none"></span>
</button>
</section>
</div><div class="tbh-26-group w-full min-h-svh flex flex-wrap">
<section class="tbh-26a flex-[1_1_340px] min-w-[300px] grid place-items-center px-6 py-14 font-tbh26-mono bg-tbh26a-stage" aria-label="Typewriter retype button">
<button type="button" class="group/btn cursor-pointer rounded-md border border-tbh26a-term px-7 py-4 text-base font-bold text-tbh26a-term bg-tbh26a-panel shadow-[0_0_20px_-8px_var(--color-tbh26a-term)] focus-visible:outline-3 focus-visible:outline-tbh26a-term focus-visible:outline-offset-4">
<span class="inline-block overflow-hidden whitespace-nowrap align-bottom w-[12ch] border-r-2 border-tbh26a-term animate-tbh26a-blink group-hover/btn:[animation:var(--animate-tbh26a-type),var(--animate-tbh26a-blink)] group-focus-visible/btn:[animation:var(--animate-tbh26a-type),var(--animate-tbh26a-blink)] motion-reduce:animate-none motion-reduce:w-[12ch]">$ deploy_now</span>
</button>
</section>
<section class="tbh-26b flex-[1_1_340px] min-w-[300px] grid place-items-center px-6 py-14 font-tbh26-mono bg-tbh26b-stage" aria-label="Prompt caret button">
<button type="button" class="group/btn inline-flex items-center gap-0.5 cursor-pointer rounded-md border border-tbh26b-term px-[30px] py-4 text-base font-bold text-tbh26b-term bg-tbh26b-panel focus-visible:outline-3 focus-visible:outline-tbh26b-term focus-visible:outline-offset-4">
<span class="overflow-hidden whitespace-nowrap max-w-[9ch] transition-[max-width] duration-700 ease-[steps(9)] group-hover/btn:max-w-0 group-focus-visible/btn:max-w-0 motion-reduce:transition-none motion-reduce:max-w-[9ch]">run build</span>
<span aria-hidden="true" class="w-[9px] h-[1.1em] bg-tbh26b-term animate-tbh26b-blink group-hover/btn:animate-tbh26b-blinkfast group-focus-visible/btn:animate-tbh26b-blinkfast motion-reduce:animate-none"></span>
</button>
</section>
</div>@import "tailwindcss";
@theme {
--color-tbh26a-term: #57e08a;
--color-tbh26a-term: oklch(0.82 0.19 145);
--color-tbh26a-stage: #06120b;
--color-tbh26a-stage: oklch(0.13 0.02 150);
--color-tbh26a-panel: #0a1c12;
--color-tbh26a-panel: oklch(0.18 0.03 150);
--color-tbh26b-term: #e0c04a;
--color-tbh26b-term: oklch(0.8 0.16 90);
--color-tbh26b-stage: #12100a;
--color-tbh26b-stage: oklch(0.15 0.02 85);
--color-tbh26b-panel: #1c180c;
--color-tbh26b-panel: oklch(0.2 0.03 85);
--font-tbh26-mono: 'JetBrains Mono', ui-monospace, Menlo, monospace;
--animate-tbh26a-type: tbh26a-type 1s steps(12) forwards;
--animate-tbh26a-blink: tbh26a-blink 1s steps(1) infinite;
--animate-tbh26b-blink: tbh26b-blink 1s steps(1) infinite;
--animate-tbh26b-blinkfast: tbh26b-blink 0.4s steps(1) infinite;
}
/* Stays CSS: the typing keyframe animates width in ch units, and the caret keyframes flip a
border colour and an opacity — none of which has a keyframe-able utility form. */
@keyframes tbh26a-type {
from {
width: 0;
}
to {
width: 12ch;
}
}
@keyframes tbh26a-blink {
50% {
border-color: transparent;
}
}
@keyframes tbh26b-blink {
50% {
opacity: 0;
}
}
.tbh-26-group {
width: 100%;
min-height: 100svh;
}
.tbh-26a,
.tbh-26b {
min-height: 100svh;
}@import "tailwindcss";
@theme {
--color-tbh26a-term: #57e08a;
--color-tbh26a-term: oklch(0.82 0.19 145);
--color-tbh26a-stage: #06120b;
--color-tbh26a-stage: oklch(0.13 0.02 150);
--color-tbh26a-panel: #0a1c12;
--color-tbh26a-panel: oklch(0.18 0.03 150);
--color-tbh26b-term: #e0c04a;
--color-tbh26b-term: oklch(0.8 0.16 90);
--color-tbh26b-stage: #12100a;
--color-tbh26b-stage: oklch(0.15 0.02 85);
--color-tbh26b-panel: #1c180c;
--color-tbh26b-panel: oklch(0.2 0.03 85);
--font-tbh26-mono: 'JetBrains Mono', ui-monospace, Menlo, monospace;
--animate-tbh26a-type: tbh26a-type 1s steps(12) forwards;
--animate-tbh26a-blink: tbh26a-blink 1s steps(1) infinite;
--animate-tbh26b-blink: tbh26b-blink 1s steps(1) infinite;
--animate-tbh26b-blinkfast: tbh26b-blink 0.4s steps(1) infinite;
}
/* Stays CSS: the typing keyframe animates width in ch units, and the caret keyframes flip a
border colour and an opacity — none of which has a keyframe-able utility form. */
@keyframes tbh26a-type {
from {
width: 0;
}
to {
width: 12ch;
}
}
@keyframes tbh26a-blink {
50% {
border-color: transparent;
}
}
@keyframes tbh26b-blink {
50% {
opacity: 0;
}
}
.tbh-26-group {
width: 100%;
min-height: 100svh;
}
.tbh-26a,
.tbh-26b {
min-height: 100svh;
}Setup — do this once per project
Skip this if your project already runs Tailwind v4.
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>Compiles in the browser — perfect for a CodePen or a quick test, not for production.
npm install tailwindcss @tailwindcss/vite
npm install tailwindcss @tailwindcss/vite// vite.config.js ("type": "module" in package.json)
import { defineConfig } from 'vite'
import tailwindcss from '@tailwindcss/vite'
export default defineConfig({
plugins: [tailwindcss()],
})// vite.config.js ("type": "module" in package.json)
import { defineConfig } from 'vite'
import tailwindcss from '@tailwindcss/vite'
export default defineConfig({
plugins: [tailwindcss()],
})Any v4 setup works — Vite, PostCSS or the CLI. Nothing in this demo is bundler-specific.
@import "tailwindcss";
@theme {
--color-tbh26a-term: #57e08a;
--color-tbh26a-term: oklch(0.82 0.19 145);
--color-tbh26a-stage: #06120b;
--color-tbh26a-stage: oklch(0.13 0.02 150);
--color-tbh26a-panel: #0a1c12;
--color-tbh26a-panel: oklch(0.18 0.03 150);
--color-tbh26b-term: #e0c04a;
--color-tbh26b-term: oklch(0.8 0.16 90);
--color-tbh26b-stage: #12100a;
--color-tbh26b-stage: oklch(0.15 0.02 85);
--color-tbh26b-panel: #1c180c;
--color-tbh26b-panel: oklch(0.2 0.03 85);
--font-tbh26-mono: 'JetBrains Mono', ui-monospace, Menlo, monospace;
--animate-tbh26a-type: tbh26a-type 1s steps(12) forwards;
--animate-tbh26a-blink: tbh26a-blink 1s steps(1) infinite;
--animate-tbh26b-blink: tbh26b-blink 1s steps(1) infinite;
--animate-tbh26b-blinkfast: tbh26b-blink 0.4s steps(1) infinite;
}
/* Stays CSS: the typing keyframe animates width in ch units, and the caret keyframes flip a
border colour and an opacity — none of which has a keyframe-able utility form. */
@keyframes tbh26a-type {
from {
width: 0;
}
to {
width: 12ch;
}
}
@keyframes tbh26a-blink {
50% {
border-color: transparent;
}
}
@keyframes tbh26b-blink {
50% {
opacity: 0;
}
}
.tbh-26-group {
width: 100%;
min-height: 100svh;
}
.tbh-26a,
.tbh-26b {
min-height: 100svh;
}@import "tailwindcss";
@theme {
--color-tbh26a-term: #57e08a;
--color-tbh26a-term: oklch(0.82 0.19 145);
--color-tbh26a-stage: #06120b;
--color-tbh26a-stage: oklch(0.13 0.02 150);
--color-tbh26a-panel: #0a1c12;
--color-tbh26a-panel: oklch(0.18 0.03 150);
--color-tbh26b-term: #e0c04a;
--color-tbh26b-term: oklch(0.8 0.16 90);
--color-tbh26b-stage: #12100a;
--color-tbh26b-stage: oklch(0.15 0.02 85);
--color-tbh26b-panel: #1c180c;
--color-tbh26b-panel: oklch(0.2 0.03 85);
--font-tbh26-mono: 'JetBrains Mono', ui-monospace, Menlo, monospace;
--animate-tbh26a-type: tbh26a-type 1s steps(12) forwards;
--animate-tbh26a-blink: tbh26a-blink 1s steps(1) infinite;
--animate-tbh26b-blink: tbh26b-blink 1s steps(1) infinite;
--animate-tbh26b-blinkfast: tbh26b-blink 0.4s steps(1) infinite;
}
/* Stays CSS: the typing keyframe animates width in ch units, and the caret keyframes flip a
border colour and an opacity — none of which has a keyframe-able utility form. */
@keyframes tbh26a-type {
from {
width: 0;
}
to {
width: 12ch;
}
}
@keyframes tbh26a-blink {
50% {
border-color: transparent;
}
}
@keyframes tbh26b-blink {
50% {
opacity: 0;
}
}
.tbh-26-group {
width: 100%;
min-height: 100svh;
}
.tbh-26a,
.tbh-26b {
min-height: 100svh;
}<link rel="preconnect" href="https://fonts.googleapis.com"> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;700&display=swap">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;700&display=swap">What changed in the port
Which parts became utilities, which stayed CSS, and why.
- Two simultaneous animations needed the one shorthand Tailwind does not model, so the hover state reads
[animation:var(--animate-tbh26a-type),var(--animate-tbh26a-blink)]— referencing the@themetokens rather than restating the timings, which keeps the durations in one place. - Design B needs no keyframe for the reveal at all:
transition-[max-width] ease-[steps(9)]gets the same character-wise clip from a transition, which is the cheaper technique and worth preferring when the copy is static. - UI change from the CSS original: both designs retype on
group-focus-visible/btn:, the caret blink rate has its own token (fast on hover), and reduced motion pins the label fully typed rather than leaving the animation's starting width.
Here's a working CSS demo from CodeFronts. Use it as-is or adapt to your framework. All classes are scoped under a unique prefix so the code won't collide with your existing styles. MIT licensed.
Demo: Tailwind Typewriter Retype Terminal Button Effect
Source: https://codefronts.com/motion/tailwind-css-button-hover-effects/tailwind-typewriter-retype-terminal-button-effect/
A retro-terminal CTA where, on hover, the label retypes character by character with a blinking caret — nostalgic and techy for dev tools and docs. The ch unit and the steps() count are the whole effect, and both are now visible in the markup where the character count lives.
## HTML
```html
<div class="tbh-26-group w-full min-h-svh flex flex-wrap">
<section class="tbh-26a flex-[1_1_340px] min-w-[300px] grid place-items-center px-6 py-14 font-tbh26-mono bg-tbh26a-stage" aria-label="Typewriter retype button">
<button type="button" class="group/btn cursor-pointer rounded-md border border-tbh26a-term px-7 py-4 text-base font-bold text-tbh26a-term bg-tbh26a-panel shadow-[0_0_20px_-8px_var(--color-tbh26a-term)] focus-visible:outline-3 focus-visible:outline-tbh26a-term focus-visible:outline-offset-4">
<span class="inline-block overflow-hidden whitespace-nowrap align-bottom w-[12ch] border-r-2 border-tbh26a-term animate-tbh26a-blink group-hover/btn:[animation:var(--animate-tbh26a-type),var(--animate-tbh26a-blink)] group-focus-visible/btn:[animation:var(--animate-tbh26a-type),var(--animate-tbh26a-blink)] motion-reduce:animate-none motion-reduce:w-[12ch]">$ deploy_now</span>
</button>
</section>
<section class="tbh-26b flex-[1_1_340px] min-w-[300px] grid place-items-center px-6 py-14 font-tbh26-mono bg-tbh26b-stage" aria-label="Prompt caret button">
<button type="button" class="group/btn inline-flex items-center gap-0.5 cursor-pointer rounded-md border border-tbh26b-term px-[30px] py-4 text-base font-bold text-tbh26b-term bg-tbh26b-panel focus-visible:outline-3 focus-visible:outline-tbh26b-term focus-visible:outline-offset-4">
<span class="overflow-hidden whitespace-nowrap max-w-[9ch] transition-[max-width] duration-700 ease-[steps(9)] group-hover/btn:max-w-0 group-focus-visible/btn:max-w-0 motion-reduce:transition-none motion-reduce:max-w-[9ch]">run build</span>
<span aria-hidden="true" class="w-[9px] h-[1.1em] bg-tbh26b-term animate-tbh26b-blink group-hover/btn:animate-tbh26b-blinkfast group-focus-visible/btn:animate-tbh26b-blinkfast motion-reduce:animate-none"></span>
</button>
</section>
</div>
```
## CSS
```css
@import "tailwindcss";
@theme {
--color-tbh26a-term: #57e08a;
--color-tbh26a-term: oklch(0.82 0.19 145);
--color-tbh26a-stage: #06120b;
--color-tbh26a-stage: oklch(0.13 0.02 150);
--color-tbh26a-panel: #0a1c12;
--color-tbh26a-panel: oklch(0.18 0.03 150);
--color-tbh26b-term: #e0c04a;
--color-tbh26b-term: oklch(0.8 0.16 90);
--color-tbh26b-stage: #12100a;
--color-tbh26b-stage: oklch(0.15 0.02 85);
--color-tbh26b-panel: #1c180c;
--color-tbh26b-panel: oklch(0.2 0.03 85);
--font-tbh26-mono: 'JetBrains Mono', ui-monospace, Menlo, monospace;
--animate-tbh26a-type: tbh26a-type 1s steps(12) forwards;
--animate-tbh26a-blink: tbh26a-blink 1s steps(1) infinite;
--animate-tbh26b-blink: tbh26b-blink 1s steps(1) infinite;
--animate-tbh26b-blinkfast: tbh26b-blink 0.4s steps(1) infinite;
}
/* Stays CSS: the typing keyframe animates width in ch units, and the caret keyframes flip a
border colour and an opacity — none of which has a keyframe-able utility form. */
@keyframes tbh26a-type {
from {
width: 0;
}
to {
width: 12ch;
}
}
@keyframes tbh26a-blink {
50% {
border-color: transparent;
}
}
@keyframes tbh26b-blink {
50% {
opacity: 0;
}
}
.tbh-26-group {
width: 100%;
min-height: 100svh;
}
.tbh-26a,
.tbh-26b {
min-height: 100svh;
}
```Here's a working CSS demo from CodeFronts. Use it as-is or adapt to your framework. All classes are scoped under a unique prefix so the code won't collide with your existing styles. MIT licensed.
Demo: Tailwind Typewriter Retype Terminal Button Effect
Source: https://codefronts.com/motion/tailwind-css-button-hover-effects/tailwind-typewriter-retype-terminal-button-effect/
A retro-terminal CTA where, on hover, the label retypes character by character with a blinking caret — nostalgic and techy for dev tools and docs. The ch unit and the steps() count are the whole effect, and both are now visible in the markup where the character count lives.
## HTML
```html
<div class="tbh-26-group w-full min-h-svh flex flex-wrap">
<section class="tbh-26a flex-[1_1_340px] min-w-[300px] grid place-items-center px-6 py-14 font-tbh26-mono bg-tbh26a-stage" aria-label="Typewriter retype button">
<button type="button" class="group/btn cursor-pointer rounded-md border border-tbh26a-term px-7 py-4 text-base font-bold text-tbh26a-term bg-tbh26a-panel shadow-[0_0_20px_-8px_var(--color-tbh26a-term)] focus-visible:outline-3 focus-visible:outline-tbh26a-term focus-visible:outline-offset-4">
<span class="inline-block overflow-hidden whitespace-nowrap align-bottom w-[12ch] border-r-2 border-tbh26a-term animate-tbh26a-blink group-hover/btn:[animation:var(--animate-tbh26a-type),var(--animate-tbh26a-blink)] group-focus-visible/btn:[animation:var(--animate-tbh26a-type),var(--animate-tbh26a-blink)] motion-reduce:animate-none motion-reduce:w-[12ch]">$ deploy_now</span>
</button>
</section>
<section class="tbh-26b flex-[1_1_340px] min-w-[300px] grid place-items-center px-6 py-14 font-tbh26-mono bg-tbh26b-stage" aria-label="Prompt caret button">
<button type="button" class="group/btn inline-flex items-center gap-0.5 cursor-pointer rounded-md border border-tbh26b-term px-[30px] py-4 text-base font-bold text-tbh26b-term bg-tbh26b-panel focus-visible:outline-3 focus-visible:outline-tbh26b-term focus-visible:outline-offset-4">
<span class="overflow-hidden whitespace-nowrap max-w-[9ch] transition-[max-width] duration-700 ease-[steps(9)] group-hover/btn:max-w-0 group-focus-visible/btn:max-w-0 motion-reduce:transition-none motion-reduce:max-w-[9ch]">run build</span>
<span aria-hidden="true" class="w-[9px] h-[1.1em] bg-tbh26b-term animate-tbh26b-blink group-hover/btn:animate-tbh26b-blinkfast group-focus-visible/btn:animate-tbh26b-blinkfast motion-reduce:animate-none"></span>
</button>
</section>
</div>
```
## CSS
```css
@import "tailwindcss";
@theme {
--color-tbh26a-term: #57e08a;
--color-tbh26a-term: oklch(0.82 0.19 145);
--color-tbh26a-stage: #06120b;
--color-tbh26a-stage: oklch(0.13 0.02 150);
--color-tbh26a-panel: #0a1c12;
--color-tbh26a-panel: oklch(0.18 0.03 150);
--color-tbh26b-term: #e0c04a;
--color-tbh26b-term: oklch(0.8 0.16 90);
--color-tbh26b-stage: #12100a;
--color-tbh26b-stage: oklch(0.15 0.02 85);
--color-tbh26b-panel: #1c180c;
--color-tbh26b-panel: oklch(0.2 0.03 85);
--font-tbh26-mono: 'JetBrains Mono', ui-monospace, Menlo, monospace;
--animate-tbh26a-type: tbh26a-type 1s steps(12) forwards;
--animate-tbh26a-blink: tbh26a-blink 1s steps(1) infinite;
--animate-tbh26b-blink: tbh26b-blink 1s steps(1) infinite;
--animate-tbh26b-blinkfast: tbh26b-blink 0.4s steps(1) infinite;
}
/* Stays CSS: the typing keyframe animates width in ch units, and the caret keyframes flip a
border colour and an opacity — none of which has a keyframe-able utility form. */
@keyframes tbh26a-type {
from {
width: 0;
}
to {
width: 12ch;
}
}
@keyframes tbh26a-blink {
50% {
border-color: transparent;
}
}
@keyframes tbh26b-blink {
50% {
opacity: 0;
}
}
.tbh-26-group {
width: 100%;
min-height: 100svh;
}
.tbh-26a,
.tbh-26b {
min-height: 100svh;
}
```How this works
steps(N) must equal the character count. steps(12) across 12ch means each step reveals exactly one glyph. Any other pairing reveals half-characters, which is the difference between a typewriter and a sliding mask — and in the port the two numbers sit in the same class list as the string they describe.
Two animations, one element. Typing is finite (forwards), blinking is infinite, and they run simultaneously. Tailwind has no utility for a two-animation shorthand, so the hover state composes them through an arbitrary [animation:...] that references both @theme tokens — which keeps the durations defined in one place.
Design B is the calmer sibling. A max-width transition with a steps(9) easing does the same character-wise reveal without a keyframe at all, and the caret is a real element that blinks faster on hover.
Make it yours
- Copy and count: change the label, then change BOTH
w-[12ch]andsteps(12)to its length. - Typing speed: the
1sin--animate-tbh26a-type; 80ms per character is the readable floor. - Caret:
border-r-2weight and theborder-tbh26a-termcolour; design B uses a 9px block instead. - Loop it: drop
forwardsand addinfinitefor a demo-reel button (not recommended in production — it never settles).
Gotchas — read before shipping
- Monospace is not optional. In a proportional face
chis the width of '0', so the reveal drifts out of sync with the glyphs. - Fix the width in
chrather than letting the label size itself, or the button's width changes as it types and the whole row reflows. - The label text stays in the DOM at all times — the animation only clips it, so the accessible name is always the full string.
- Under
motion-reduce:both designs pin the label fully typed (motion-reduce:w-[12ch],motion-reduce:max-w-[9ch]) and stop the caret; a frozen half-typed label would look like a bug.
Browser support
| Chrome | Safari | Firefox | Edge |
|---|---|---|---|
| 111+ | 16.4+ | 128+ | 111+ |
steps(), ch units and width keyframes are universal; with animation unavailable the full label shows, which is the correct resting state.