Topple
const WORD = 'Explore';
const css = `
/* Each letter tips onto the next, holds a moment, then stands back up. */
@keyframes btn-topple-tip {
0% { transform: none; }
22% { transform: rotate(24deg); }
30% { transform: rotate(19deg); }
62% { transform: rotate(19deg); }
80% { transform: rotate(-5deg); }
100% { transform: none; }
}
.btn-topple {
display: inline-flex;
align-items: center;
gap: 10px;
height: 48px;
padding: 0 22px 0 24px;
border: none;
border-radius: 999px;
background: light-dark(#373a40, #f1f2f3);
color: light-dark(#fbfcfd, #121417);
font: inherit;
font-size: 15px;
font-weight: 600;
cursor: pointer;
box-shadow: 0 10px 22px -12px light-dark(rgba(0, 0, 0, 0.18), rgba(0, 0, 0, 0.6));
}
.btn-topple:focus-visible { outline: 2px solid light-dark(#50535b, #c6c8cd); outline-offset: 3px; }
.btn-topple:active { transform: scale(0.97); }
.btn-topple__word { display: inline-flex; }
.btn-topple__ch { display: inline-block; transform-origin: 100% 100%; }
.btn-topple:is(:hover, :focus-visible) .btn-topple__ch { animation: btn-topple-tip 1.1s cubic-bezier(0.5, 0, 0.3, 1) calc(var(--i) * 55ms) both; }
.btn-topple__dot { width: 7px; height: 7px; border-radius: 50%; background: currentColor; transition: transform 0.3s; }
.btn-topple:is(:hover, :focus-visible) .btn-topple__dot { transform: translateX(3px); }
@media (prefers-reduced-motion: reduce) {
.btn-topple__ch { animation: none !important; }
}
`;
export function ToppleButton() {
return (
<>
<style>
{css}
</style>
<button type="button" className="btn-topple" aria-label={WORD}>
<span className="btn-topple__word" aria-hidden="true">
{[...WORD].map((c, i) => (
<span key={i} className="btn-topple__ch" style={{ '--i': i } as React.CSSProperties}>
{c}
</span>
))}
</span>
<span className="btn-topple__dot" aria-hidden="true" />
</button>
</>
);
}Markup where the component goes, CSS in your stylesheet. No JavaScript.
<button type="button" class="btn-topple" aria-label="Explore">
<span class="btn-topple__word" aria-hidden="true">
<span class="btn-topple__ch" style="--i:0">E</span>
<span class="btn-topple__ch" style="--i:1">x</span>
<span class="btn-topple__ch" style="--i:2">p</span>
<span class="btn-topple__ch" style="--i:3">l</span>
<span class="btn-topple__ch" style="--i:4">o</span>
<span class="btn-topple__ch" style="--i:5">r</span>
<span class="btn-topple__ch" style="--i:6">e</span>
</span>
<span class="btn-topple__dot" aria-hidden="true"></span>
</button>/* Each letter tips onto the next, holds a moment, then stands back up. */
@keyframes btn-topple-tip {
0% { transform: none; }
22% { transform: rotate(24deg); }
30% { transform: rotate(19deg); }
62% { transform: rotate(19deg); }
80% { transform: rotate(-5deg); }
100% { transform: none; }
}
.btn-topple {
display: inline-flex;
align-items: center;
gap: 10px;
height: 48px;
padding: 0 22px 0 24px;
border: none;
border-radius: 999px;
background: light-dark(#373a40, #f1f2f3);
color: light-dark(#fbfcfd, #121417);
font: inherit;
font-size: 15px;
font-weight: 600;
cursor: pointer;
box-shadow: 0 10px 22px -12px light-dark(rgba(0, 0, 0, 0.18), rgba(0, 0, 0, 0.6));
}
.btn-topple:focus-visible { outline: 2px solid light-dark(#50535b, #c6c8cd); outline-offset: 3px; }
.btn-topple:active { transform: scale(0.97); }
.btn-topple__word { display: inline-flex; }
.btn-topple__ch { display: inline-block; transform-origin: 100% 100%; }
.btn-topple:is(:hover, :focus-visible) .btn-topple__ch { animation: btn-topple-tip 1.1s cubic-bezier(0.5, 0, 0.3, 1) calc(var(--i) * 55ms) both; }
.btn-topple__dot { width: 7px; height: 7px; border-radius: 50%; background: currentColor; transition: transform 0.3s; }
.btn-topple:is(:hover, :focus-visible) .btn-topple__dot { transform: translateX(3px); }
@media (prefers-reduced-motion: reduce) {
.btn-topple__ch { animation: none !important; }
}Goal: Create a "Topple" button.
Visual behavior: An explore button whose letters tip over like dominoes on hover, each knocking the next, hold a moment and then stand back up in a ripple.
Technique:
- Each letter is its own span with an index (--i) and a transform-origin at its bottom right, so it falls onto the next.
- One keyframe tips it, holds and rights it again, delayed by the index; the button carries aria-label with the whole word and the letters are aria-hidden.
Accessibility: A real <button> with a visible label; decoration is aria-hidden. The button is named by aria-label, since the label is split into letters. Keyboard focus plays the same ripple as hover. Under prefers-reduced-motion the letters stay upright.
My stack: {{USER_STACK}}
My styling: {{USER_STYLING}}
My constraints: {{USER_CONSTRAINTS}}
My colors: neutral only. Black, white and grays, no hues. Use the color values above as written, or my project's closest neutral tokens.
Return a single self-contained component. No dependencies.Goal: Create a "Topple" button.
Visual behavior: An explore button whose letters tip over like dominoes on hover, each knocking the next, hold a moment and then stand back up in a ripple.
Technique:
- Each letter is its own span with an index (--i) and a transform-origin at its bottom right, so it falls onto the next.
- One keyframe tips it, holds and rights it again, delayed by the index; the button carries aria-label with the whole word and the letters are aria-hidden.
Accessibility: A real <button> with a visible label; decoration is aria-hidden. The button is named by aria-label, since the label is split into letters. Keyboard focus plays the same ripple as hover. Under prefers-reduced-motion the letters stay upright.
My stack: {{USER_STACK}}
My styling: {{USER_STYLING}}
My constraints: {{USER_CONSTRAINTS}}
My colors: use my project's existing color tokens. The color values above are grayscale placeholders. Map each one by its role (surface, text, border, accent, glow) to my theme, and keep their relative lightness so contrast and the effect still read.
Return a single self-contained component. No dependencies.Paste into Claude, ChatGPT or Cursor after filling in {{USER_STACK}}, {{USER_STYLING}} and {{USER_CONSTRAINTS}}. Neutral keeps the greys you see here; Match my project asks the model to map them onto your own colour tokens.
Free to use in any project, commercial included. MIT-0, no attribution required. Licence
About this component
An explore button whose letters tip over like dominoes on hover, each knocking the next, hold a moment and then stand back up in a ripple.
- Dependencies
- None
- Difficulty
- Beginner
- Triggers
- hover, delay stagger, keyframe
- Techniques
- keyframes, css variables, stagger, text animation
- Reduced motion
- Respected
- Theme
- Light and dark (follows your page's color-scheme)
A real <button> with a visible label; decoration is aria-hidden. The button is named by aria-label, since the label is split into letters. Keyboard focus plays the same ripple as hover. Under prefers-reduced-motion the letters stay upright.