AnimatedButton
Smooth width transitions when button content changes.
hooks/use-measure.ts
import React from "react"
interface Bounds {
width: number
height: number
}
type UseMeasureReturn<T extends HTMLElement> = [(node: T | null) => void, Bounds]
export function useMeasure<T extends HTMLElement = HTMLElement>(): UseMeasureReturn<T> {
const [element, setElement] = React.useState<T | null>(null)
const [bounds, setBounds] = React.useState<Bounds>({ width: 0, height: 0 })
const ref = React.useCallback((node: T | null) => {
setElement(node)
}, [])
React.useEffect(() => {
if (!element) return
const observer = new ResizeObserver(([entry]) => {
if (!entry) return
setBounds({
width: entry.contentRect.width,
height: entry.contentRect.height,
})
})
observer.observe(element)
return () => observer.disconnect()
}, [element])
return [ref, bounds]
}
components/ui/animated-button.tsx
"use client"
import { useMeasure } from "@/hooks/use-measure"
import React from "react"
const EASING = "cubic-bezier(0.19,1,0.22,1)"
const transitionIn = [
`opacity 500ms ease-in-out 50ms`,
`filter 400ms ${EASING} 50ms`,
`scale 400ms ${EASING} 50ms`,
].join(", ")
export function AnimatedButton({ children, className, ...props }: React.ComponentProps<"button">) {
const [ref, bounds] = useMeasure()
const [width, setWidth] = React.useState("auto")
const [displayedChildren, setDisplayedChildren] = React.useState(children)
const [visible, setVisible] = React.useState(true)
const isFirstRender = React.useRef(true)
React.useEffect(() => {
if (bounds.width > 0) setWidth(`${bounds.width}px`)
}, [bounds.width])
React.useEffect(() => {
if (isFirstRender.current) {
isFirstRender.current = false
return
}
setDisplayedChildren(children)
setVisible(false)
requestAnimationFrame(() => {
requestAnimationFrame(() => {
setVisible(true)
})
})
}, [children])
return (
<button
style={{ width }}
className={`bg-primary flex cursor-pointer items-center justify-center rounded-lg border py-1.5 transition-all duration-300 ease-[cubic-bezier(0.19,1,0.22,1)] active:scale-95 ${className ?? ""}`}
{...props}
>
<div ref={ref}>
<span
style={{
display: "inline-block",
opacity: visible ? 1 : 0,
filter: visible ? "blur(0px)" : "blur(8px)",
scale: visible ? 1 : 0.95,
transition: visible ? transitionIn : "none",
}}
className="text-primary-foreground px-4 text-sm font-medium whitespace-nowrap"
>
{displayedChildren}
</span>
</div>
</button>
)
}
components/examples/animated-button.tsx
"use client"
import { AnimatedButton } from "@/components/ui/animated-button"
import React from "react"
const LABELS = ["Click me", "I'm clicked!", "Clicked again!"]
export function AnimatedButtonExample() {
const [index, setIndex] = React.useState(0)
return <AnimatedButton onClick={() => setIndex((prev) => (prev + 1) % LABELS.length)}>{LABELS[index]}</AnimatedButton>
}