Button Library · Signature
Split-Flap
A railway board: letters flip through and land on the word.
Each letter is its own tile, so the words live in the markup, not in React children.
States
Default
Hover
Active
Focus
Code
sig-flap.html
<button class="btn-sig-flap" type="button" aria-label="Depart"> <span class="btn-sig-flap__flap" data-flap aria-hidden="true">D</span> <span class="btn-sig-flap__flap" data-flap aria-hidden="true">E</span> <span class="btn-sig-flap__flap" data-flap aria-hidden="true">P</span> <span class="btn-sig-flap__flap" data-flap aria-hidden="true">A</span> <span class="btn-sig-flap__flap" data-flap aria-hidden="true">R</span> <span class="btn-sig-flap__flap" data-flap aria-hidden="true">T</span> </button>
sig-flap.css
.btn-sig-flap {
--btn-bg: #1b1b22;
--btn-fg: #ffd23f;
--btn-font: ui-monospace, monospace;
--btn-size: 17px;
--btn-weight: 700;
--btn-w: auto;
--btn-h: auto;
--btn-bw: 0px;
--btn-line: #8b6cff;
--btn-radius: 10px;
--btn-shadow: none;
--btn-speed: 180ms;
}
.btn-sig-flap {
position: relative;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 3px;
box-sizing: border-box;
width: var(--btn-w);
height: var(--btn-h);
padding: 0;
font-family: var(--btn-font);
font-size: var(--btn-size);
font-weight: var(--btn-weight);
line-height: 1.2;
color: var(--btn-fg);
background: transparent;
border: var(--btn-bw) solid var(--btn-line);
border-radius: var(--btn-radius);
box-shadow: var(--btn-shadow);
cursor: pointer;
text-decoration: none;
white-space: nowrap;
transition: all var(--btn-speed) ease;
perspective: 300px;
}
.btn-sig-flap:focus-visible {
outline: 2px solid var(--btn-bg);
outline-offset: 2px;
}
.btn-sig-flap:disabled {
opacity: 0.5;
pointer-events: none;
}
.btn-sig-flap__icon {
width: 1.15em;
height: 1.15em;
flex-shrink: 0;
}
.btn-sig-flap__flap {
display: grid;
place-items: center;
width: 1.35em;
height: 1.9em;
background: linear-gradient(transparent 48%, rgba(0,0,0,.75) 48% 52%, transparent 52%), linear-gradient(180deg, color-mix(in srgb, var(--btn-bg) 86%, #fff), var(--btn-bg));
border-radius: 4px;
box-shadow: 0 2px 0 rgba(0,0,0,.5);
}
@media (prefers-reduced-motion: reduce) {
.btn-sig-flap {
animation: none !important;
transition: none !important;
}
}
sig-flap.js
document.querySelectorAll(".btn-sig-flap").forEach((el) => {
const flaps = Array.from(el.querySelectorAll("[data-flap]"));
const word = flaps.map((f) => f.textContent);
const CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
let timer = 0;
let step = 0;
const stop = () => {
clearInterval(timer);
timer = 0;
flaps.forEach((f, i) => {
f.textContent = word[i];
});
};
const turn = () => {
step += 1;
flaps.forEach((f, i) => {
const left = i * 2 + 6 - step;
if (left < -1) return;
f.textContent = left < 0 ? word[i] : CHARS[Math.floor(Math.random() * CHARS.length)];
f.animate([{ transform: "rotateX(-80deg)" }, { transform: "rotateX(0)" }], { duration: 60 });
});
if (step > flaps.length * 2 + 6) stop();
};
const start = () => {
if (timer || matchMedia("(prefers-reduced-motion: reduce)").matches) return;
step = 0;
timer = setInterval(turn, 70);
};
el.addEventListener("pointerenter", start);
el.addEventListener("focus", start);
return () => {
stop();
el.removeEventListener("pointerenter", start);
el.removeEventListener("focus", start);
};
});
sig-flap.tsx
import { useEffect, useRef } from "react";
import type React from "react";
import "./sig-flap.css"; // the CSS tab
function attach(el: HTMLElement) {
const flaps = Array.from(el.querySelectorAll("[data-flap]")) as HTMLElement[];
const word = flaps.map((f) => f.textContent);
const CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
let timer = 0;
let step = 0;
const stop = () => {
clearInterval(timer);
timer = 0;
flaps.forEach((f, i) => {
f.textContent = word[i];
});
};
const turn = () => {
step += 1;
flaps.forEach((f, i) => {
const left = i * 2 + 6 - step;
if (left < -1) return;
f.textContent = left < 0 ? word[i] : CHARS[Math.floor(Math.random() * CHARS.length)];
f.animate([{ transform: "rotateX(-80deg)" }, { transform: "rotateX(0)" }], { duration: 60 });
});
if (step > flaps.length * 2 + 6) stop();
};
const start = () => {
if (timer || matchMedia("(prefers-reduced-motion: reduce)").matches) return;
step = 0;
timer = setInterval(turn, 70);
};
el.addEventListener("pointerenter", start);
el.addEventListener("focus", start);
return () => {
stop();
el.removeEventListener("pointerenter", start);
el.removeEventListener("focus", start);
};
}
export function BtnSigFlap(props: React.ComponentProps<"button">) {
const ref = useRef<HTMLButtonElement>(null);
useEffect(() => attach(ref.current!), []);
return (
<button className="btn-sig-flap" type="button" aria-label="Depart" ref={ref} {...props}>
<span className="btn-sig-flap__flap" data-flap aria-hidden="true">{"D"}</span>
<span className="btn-sig-flap__flap" data-flap aria-hidden="true">{"E"}</span>
<span className="btn-sig-flap__flap" data-flap aria-hidden="true">{"P"}</span>
<span className="btn-sig-flap__flap" data-flap aria-hidden="true">{"A"}</span>
<span className="btn-sig-flap__flap" data-flap aria-hidden="true">{"R"}</span>
<span className="btn-sig-flap__flap" data-flap aria-hidden="true">{"T"}</span>
</button>
);
}
sig-flap.html
<button class="btn-sig-flap relative inline-flex items-center justify-center gap-[3px] [box-sizing:border-box] [padding:0] [font-family:ui-monospace,_monospace] text-[17px] font-[700] [line-height:1.2] text-[#ffd23f] [background:transparent] [border:0px_solid_#8b6cff] rounded-[10px] [box-shadow:none] cursor-pointer no-underline whitespace-nowrap [transition:all_180ms_ease] [perspective:300px] focus-visible:[outline:2px_solid_#1b1b22] focus-visible:[outline-offset:2px] disabled:[opacity:0.5] disabled:[pointer-events:none] motion-reduce:[transition:none!important]" type="button" aria-label="Depart"> <span class="grid [place-items:center] w-[1.35em] h-[1.9em] [background:linear-gradient(transparent_48%,_rgba(0,0,0,.75)_48%_52%,_transparent_52%),_linear-gradient(180deg,_color-mix(in_srgb,_#1b1b22_86%,_#fff),_#1b1b22)] rounded-[4px] [box-shadow:0_2px_0_rgba(0,0,0,.5)]" data-flap aria-hidden="true">D</span> <span class="grid [place-items:center] w-[1.35em] h-[1.9em] [background:linear-gradient(transparent_48%,_rgba(0,0,0,.75)_48%_52%,_transparent_52%),_linear-gradient(180deg,_color-mix(in_srgb,_#1b1b22_86%,_#fff),_#1b1b22)] rounded-[4px] [box-shadow:0_2px_0_rgba(0,0,0,.5)]" data-flap aria-hidden="true">E</span> <span class="grid [place-items:center] w-[1.35em] h-[1.9em] [background:linear-gradient(transparent_48%,_rgba(0,0,0,.75)_48%_52%,_transparent_52%),_linear-gradient(180deg,_color-mix(in_srgb,_#1b1b22_86%,_#fff),_#1b1b22)] rounded-[4px] [box-shadow:0_2px_0_rgba(0,0,0,.5)]" data-flap aria-hidden="true">P</span> <span class="grid [place-items:center] w-[1.35em] h-[1.9em] [background:linear-gradient(transparent_48%,_rgba(0,0,0,.75)_48%_52%,_transparent_52%),_linear-gradient(180deg,_color-mix(in_srgb,_#1b1b22_86%,_#fff),_#1b1b22)] rounded-[4px] [box-shadow:0_2px_0_rgba(0,0,0,.5)]" data-flap aria-hidden="true">A</span> <span class="grid [place-items:center] w-[1.35em] h-[1.9em] [background:linear-gradient(transparent_48%,_rgba(0,0,0,.75)_48%_52%,_transparent_52%),_linear-gradient(180deg,_color-mix(in_srgb,_#1b1b22_86%,_#fff),_#1b1b22)] rounded-[4px] [box-shadow:0_2px_0_rgba(0,0,0,.5)]" data-flap aria-hidden="true">R</span> <span class="grid [place-items:center] w-[1.35em] h-[1.9em] [background:linear-gradient(transparent_48%,_rgba(0,0,0,.75)_48%_52%,_transparent_52%),_linear-gradient(180deg,_color-mix(in_srgb,_#1b1b22_86%,_#fff),_#1b1b22)] rounded-[4px] [box-shadow:0_2px_0_rgba(0,0,0,.5)]" data-flap aria-hidden="true">T</span> </button> <!-- Needs the script from the JS tab. -->