ButtonsSimpleApril 8, 2026
Button Bubble Arrow
On hover, a leading arrow circle scales in from the left while the duplicate arrow on the right scales out. The label slides from behind the right arrow using a translateX transition. Pure CSS.
View Full Demo →Preview
Source
demo.jsx
import BtnBubbleArrow from "./index.jsx";
import { btnBubbleArrow } from "@/content/buttons/demo-data.js";
import styles from "./demo.module.css";
export default function BtnBubbleArrowDemo() {
return (
<div className={styles.demo}>
<div className={styles.hero}>
<p className={styles.label}>No. 5 Studio</p>
<h1 className={styles.heading}>Ready to start<br />a project?</h1>
<BtnBubbleArrow {...btnBubbleArrow} />
</div>
</div>
);
}
index.jsx
"use client";
import styles from "./styles.module.css";
const Arrow = () => (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
width="100%"
className={styles.svg}
>
<polyline
points="18 8 18 18 8 18"
fill="none"
stroke="currentColor"
strokeMiterlimit="10"
strokeWidth="1.5"
/>
<line
x1="18"
y1="18"
x2="5"
y2="5"
fill="none"
stroke="currentColor"
strokeMiterlimit="10"
strokeWidth="1.5"
/>
</svg>
);
export default function BtnBubbleArrow({ buttons = [] }) {
return (
<div className={styles.group}>
{buttons.map((btn, i) => (
<a key={i} href={btn.href} className={styles.btn}>
{/* Leading arrow — scales in from left on hover */}
<div className={styles.arrow}>
<Arrow />
</div>
{/* Label — slides from behind the duplicate arrow */}
<div className={styles.content}>
<span className={styles.text}>{btn.label}</span>
</div>
{/* Duplicate arrow — sits on top, scales out on hover */}
<div className={`${styles.arrow} ${styles.arrowDuplicate}`}>
<Arrow />
</div>
</a>
))}
</div>
);
}
demo.module.css
.demo {
min-height: 100vh;
background: #f5f5f0;
display: flex;
align-items: center;
justify-content: center;
}
.hero {
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
gap: 3rem;
padding: 4rem 2rem;
}
.label {
font-size: 0.75rem;
font-weight: 500;
letter-spacing: 0.14em;
text-transform: uppercase;
color: #9b9b9b;
}
.heading {
font-size: clamp(3rem, 8vw, 7rem);
font-weight: 500;
letter-spacing: -0.03em;
line-height: 0.95;
color: #1a1a1a;
}
styles.module.css
.group {
display: flex;
flex-wrap: wrap;
gap: 3em;
justify-content: center;
font-size: 1.25rem;
padding: 4rem 2rem;
background: #f5f5f0;
min-height: 100%;
align-items: center;
}
.btn {
border-radius: 10em;
justify-content: center;
align-items: center;
font-size: 1em;
text-decoration: none;
display: flex;
position: relative;
}
.arrow {
color: #131313;
background-color: #ff4c2f;
border-radius: 10em;
display: flex;
flex-flow: row;
justify-content: center;
align-items: center;
width: 3.75em;
height: 3.75em;
position: relative;
transition: transform 0.735s cubic-bezier(0.625, 0.05, 0, 1);
transform: scale(0) rotate(0.001deg);
transform-origin: left;
}
/* Duplicate arrow — positioned absolute to right, starts visible, hides on hover */
.arrowDuplicate {
background-color: #efeeec;
position: absolute;
right: 0;
z-index: 2;
transform: scale(1) rotate(0.001deg);
transform-origin: right;
}
.svg {
width: 40%;
transition: transform 0.735s cubic-bezier(0.625, 0.05, 0, 1);
transform: rotate(0.001deg);
}
.content {
color: #efeeec;
background-color: rgba(0, 0, 0, 0.4);
border-radius: 10em;
display: flex;
justify-content: center;
align-items: center;
height: 3.75em;
padding-left: 2em;
padding-right: 2em;
position: relative;
transition: transform 0.735s cubic-bezier(0.625, 0.05, 0, 1);
transform: translateX(-3.75em) rotate(0.001deg);
}
.text {
line-height: 1;
white-space: nowrap;
}
/* ── Hover states ── */
.btn:hover .content {
transform: translateX(0em) rotate(0.001deg);
}
.btn:hover .svg {
transform: rotate(-45deg);
}
.btn:hover .arrow:not(.arrowDuplicate) {
transform: scale(1) rotate(0.001deg);
}
.btn:hover .arrowDuplicate {
transform: scale(0) rotate(0.001deg);
}