Rolling Text Button
A typographic button whose letters roll into place, one small detail at a time.
'use client';
import { forwardRef, type ButtonHTMLAttributes, type CSSProperties } from 'react';
import './rolling-text-button.css';
export interface RollingTextButtonProps
extends Omit<ButtonHTMLAttributes<HTMLButtonElement>, 'children'> {
/** A plain-text label. Its decorative letters roll while its accessible name stays fixed. */
children: string;
}
/** A typographic action with a staggered, vertical letter roll. */
export const RollingTextButton = forwardRef<HTMLButtonElement, RollingTextButtonProps>(
function RollingTextButton(
{ children, className = '', type = 'button', disabled = false, ...props },
ref,
) {
let letterIndex = 0;
return (
<button
{...props}
ref={ref}
type={type}
disabled={disabled}
className={`ac-rolling-text ${className}`.trim()}
>
<span className="ac-rolling-text__accessible">{children}</span>
<span className="ac-rolling-text__visual" aria-hidden="true">
{children.split(/(\s+)/).map((word, wordIndex) => {
if (!word || /^\s+$/.test(word)) return word;
return (
<span key={wordIndex} className="ac-rolling-text__word">
{Array.from(word).map((character, index) => {
const delay = Math.min(letterIndex++, 16) * 16;
return (
<span
key={index}
className="ac-rolling-text__window"
style={{ '--ac-roll-delay': `${delay}ms` } as CSSProperties}
>
<span className="ac-rolling-text__glyph">{character}</span>
<span className="ac-rolling-text__glyph">{character}</span>
</span>
);
})}
</span>
);
})}
</span>
</button>
);
},
);
export default RollingTextButton;
Installation
Run this in your shadcn-configured React project. The component and stylesheet are added together.
npx shadcn@latest add http://localhost:5173/r/rolling-text-button.jsonThis command uses the current site address. For localhost, keep this preview running on the same computer. Files follow your configured components alias.
Usage
Import the component and make the details your own.
'use client';
import { useState } from 'react';
import { RollingTextButton } from '@/components/anticraft/RollingTextButton';
export function Example() {
const [count, setCount] = useState(0);
return (
<div>
<RollingTextButton onClick={() => setCount(value => value + 1)}>
Make your move
</RollingTextButton>
<p role="status">{count ? `Demo action activated ${count} times.` : 'Try the button.'}</p>
</div>
);
}Props
| Prop | Type | Default | Description |
|---|---|---|---|
children | string | Required | The visible label. Use plain text; the component supplies a single accessible name. |
type | 'button' | 'submit' | 'reset' | 'button' | The native button type. Set submit explicitly when the button should submit its associated form. |
disabled | boolean | false | Disables native activation and decorative hover/pressed effects. The live demo includes a checkbox to inspect this state. |
onClick | MouseEventHandler<HTMLButtonElement> | undefined | Your action handler. Native keyboard and touch activation use the same handler. Buttons have no built-in navigation, payment, or form-submission action. |
className / style | string / CSSProperties | '' / undefined | Additional classes and native inline styles. Theme with --ac-roll-bg, --ac-roll-color, --ac-roll-border, --ac-roll-radius, --ac-roll-hover-bg, and --ac-roll-focus. |
Native button props / ref | ButtonHTMLAttributes<HTMLButtonElement> / Ref<HTMLButtonElement> | undefined | Native form, name, value, aria-*, data-*, event, and other button attributes pass through. A forwarded ref points to the actual button. Native disabled behavior is preserved. |
Motion & accessibility
A plain-text label rolls in staggered letter pairs on fine-pointer hover or keyboard focus. The accessible label occurs once. Motion is finite and removed for prefers-reduced-motion. Every variant keeps a visible focus outline, a native button hit area of at least 44px, and a disabled state. In the demo, activation increments a clearly labeled demo counter; connect your own action in your app.
Use semantic content and meaningful labels in your composition. Check contrast and keyboard behavior with your own styles and content.
Inspiration
Noomo’s rounded contact action and repeated visual Send label informed this typographic button study. Anticraft adds an original per-letter roll with one accessible name, a native disabled state, and reduced-motion support.
Visit Noomo Agency — contact CTAThe component source and demo artwork are original to Anticraft UI. The linked site is the design or content reference.