Rolling Text Button

On hover each letter of the label rolls up and is replaced by a copy from below, in a quick wave from left to right.

  • CSS
  • Buttons
  • Hover
  • Added

AI prompt

Create a button whose label rolls letter by letter on hover, in pure CSS with a React split. Split the label into one span per character (keep spaces as non-breaking spaces) and give each span text-shadow: 0 1.3em 0 currentColor, which draws an identical copy one line below. Wrap the letters in an inline-flex span with height 1.3em and overflow: hidden so only one line shows, and give each letter a line-height of 1.3em. On :hover and :focus-visible, translate every letter to translateY(-1.3em) over 400ms with cubic-bezier(0.16, 1, 0.3, 1), and set transition-delay to index × 25ms via a CSS variable so the roll sweeps left to right. Put the full label in aria-label and mark the letter spans aria-hidden. Under prefers-reduced-motion, keep the label still and change only the background. The label reads "Get in touch".

Source

Demo.tsx
'use client'

import s from './demo.module.css'

export default function RollingText({ labels }: { labels: Record<string, string> }) {
  return (
    <button type="button" className={s.button} aria-label={labels.cta}>
      <span className={s.label}>
        {Array.from(labels.cta).map((c, i) => (
          <span key={i} className={s.char} style={{ '--i': i } as React.CSSProperties} aria-hidden="true">
            {c === ' ' ? ' ' : c}
          </span>
        ))}
      </span>
    </button>
  )
}
demo.module.css
.button {
  display: inline-flex;
  align-items: center;
  height: 52px;
  padding: 0 30px;
  border: 0;
  border-radius: 999px;
  background: var(--stage-ink);
  color: var(--stage-bg);
  font: 600 var(--text-base) / 1.3 var(--font-body);
  white-space: nowrap;
  cursor: pointer;
  transition: background-color var(--dur-short);
}

.button:focus-visible { outline: 2px solid var(--color-focus); outline-offset: 3px; }

/* clip to exactly one line so the shadow copy below stays hidden */
.label {
  display: inline-flex;
  height: 1.3em;
  overflow: hidden;
}

.char {
  display: inline-block;
  line-height: 1.3em;
  text-shadow: 0 1.3em 0 currentColor;
  transition: transform 400ms var(--ease-out);
  transition-delay: calc(var(--i) * 25ms);
}

.button:hover .char,
.button:focus-visible .char { transform: translateY(-1.3em); }

@media (prefers-reduced-motion: reduce) {
  .char { transition: none; }
  .button:hover .char, .button:focus-visible .char { transform: none; }
  .button:hover { background: var(--stage-accent); }
}

Suggest an idea

Seen a motion or a theme you would like to find here? Describe it, and it opens as a prefilled GitHub issue for you to post.

What are you suggesting?

Needs a GitHub account. Suggestions are public.