Typewriter
loomui/typewriterFreeComponent
Phrases typed out and deleted in a loop, with a caret and no layout shift.
Live preview
live · rendered by the registry
Rendered by loomui on their own page — this is the component running, not a screenshot of it.Install it
npx shadcn@latest add https://loomui.design/r/typewriter.jsonSource
1"use client"23import * as React from "react"45import { cn } from "@/lib/utils"67export interface TypewriterProps extends Omit<8 React.ComponentProps<"span">,9 "children"10> {11 /** Phrases typed in order. A single phrase types once and stops. */12 words: string[]13 /** Milliseconds per character while typing. */14 typingSpeed?: number15 /** Milliseconds per character while deleting. Deletes read faster. */16 deletingSpeed?: number17 /** Milliseconds held on a finished phrase before it deletes. */18 holdDelay?: number19 /** Start over after the last phrase. */20 loop?: boolean21 /** Show the blinking caret. */22 caret?: boolean23 /** Render the first phrase in full with no typing. */24 disabled?: boolean25}2627export function Typewriter({28 words,29 typingSpeed = 70,30 deletingSpeed = 40,31 holdDelay = 1600,32 loop = true,33 caret = true,34 disabled = false,35 className,36 ...props37}: TypewriterProps) {38 const phrases = words.length > 0 ? words : [""]39 const longest = React.useMemo(40 () => phrases.reduce((a, b) => (a.length >= b.length ? a : b), ""),41 [phrases]42 )4344 const [index, setIndex] = React.useState(0)45 const [text, setText] = React.useState("")46 const [deleting, setDeleting] = React.useState(false)47 const [enabled, setEnabled] = React.useState(false)4849 // Typing is a text change, not a motion effect, but it is still motion to50 // anyone who asked for less of it.51 React.useEffect(() => {52 if (disabled) {53 return54 }55 const query = window.matchMedia("(prefers-reduced-motion: reduce)")56 setEnabled(!query.matches)5758 const onChange = (event: MediaQueryListEvent) => setEnabled(!event.matches)59 query.addEventListener("change", onChange)60 return () => query.removeEventListener("change", onChange)61 }, [disabled])6263 React.useEffect(() => {64 if (!enabled) {65 return66 }6768 const current = phrases[index % phrases.length]69 const done = text === current70 const atLast = index === phrases.length - 17172 if (done && !deleting) {73 if (!loop && atLast) {74 return75 }76 const timer = setTimeout(() => setDeleting(true), holdDelay)77 return () => clearTimeout(timer)78 }7980 if (deleting && text === "") {81 setDeleting(false)82 setIndex((previous) => (previous + 1) % phrases.length)83 return84 }8586 const timer = setTimeout(87 () => {88 setText((previous) =>89 deleting90 ? current.slice(0, previous.length - 1)91// … truncated
What it pulls in
Other registry items
Files it writes
More from loomui
All 91 items| Component | Registry | Kind | Access | Installs | Command |
|---|---|---|---|---|---|
| Aurora Backdropaurora-backdrop | loomui | Components | Free | no deps | |
| Bento Gridbento-grid | loomui | Components | Free | no deps | |
| Card Stackcard-stack | loomui | Components | Free | no deps | |
| Compare Slidercompare-slider | loomui | Components | Free | no deps | |
| Confetti Buttonconfetti-button | loomui | Components | Free | no deps | |
| Count Upcount-up | loomui | Components | Free | no deps | |
| Credit Cardcredit-card | loomui | Components | Free | no deps | |
| Drawerdrawer | loomui | Components | Free | 1 dep |
