Back to experiments

Solid Ripple Button

Ripple button in Solid.js with Tailwind
Tailwind config
tsx
{
theme: {
extend: {
animation: {
ripple: "ripple 0.8s ease-out forwards",
},
keyframes: {
ripple: {
"0%": {
transform: "scale(1)",
opacity: 1
},
"60%": {
transform: "scale(var(--scale))"
},
"100%": {
transform: "scale(var(--scale))",
opacity: 0
}
}
}
}
}
};
tsx
const RIPPLE_RADIUS = 8;
const Button: Component<ComponentProps<"button">> = (props) => {
const [local, others] = splitProps(props, ["children"]);
let ref: HTMLButtonElement | undefined;
const [ripples, setRipples] = createStore<{
[key: string]: { x: number; y: number; scale: number; visible: boolean };
}>({});
function createRipple(event: MouseEvent) {
if (!ref) return;
const rect = ref.getBoundingClientRect();
let x: number, y: number;
// Keyboard
if ((event as PointerEvent).pointerType === "") {
x = rect.width / 2;
y = rect.height / 2;
} else {
x = event.clientX - rect.left;
y = event.clientY - rect.top;
}
const radius = Math.sqrt(Math.max(x, rect.width - x) ** 2 + Math.max(y, rect.height - y) ** 2);
const scale = radius / RIPPLE_RADIUS;
setRipples(createUniqueId(), { x, y, scale, visible: true });
}
function rippleEnd(event: AnimationEvent) {
if (event.animationName !== "ripple" || !event.target) return;
const target = event.target as HTMLSpanElement;
setRipples(
produce((draft) => {
delete draft[target.id];
})
);
}
return (
<button
class="relative overflow-hidden bg-zinc-900 px-12 py-3 font-brand font-medium ring-1 ring-inset ring-cyan-100/5 transition hover:bg-zinc-800 hover:ring-cyan-100/10"
ref={ref}
onClick={createRipple}
{...others}
>
{local.children}
<For each={Object.keys(ripples)}>
{(key) => {
const ripple = () => ripples[key];
return (
<Show when={ripple().visible}>
<span
id={key}
onAnimationEnd={rippleEnd}
style={{
top: `${ripple().y - RIPPLE_RADIUS}px`,
left: `${ripple().x - RIPPLE_RADIUS}px`,
width: `${RIPPLE_RADIUS * 2}px`,
height: `${RIPPLE_RADIUS * 2}px`,
"--scale": ripple().scale,
}}
class="absolute animate-ripple rounded-full bg-cyan-500/30 mix-blend-screen"
/>
</Show>
);
}}
</For>
</button>
);
}