Skip to content

Instantly share code, notes, and snippets.

@NamesMT
Last active November 1, 2025 08:24
Show Gist options
  • Select an option

  • Save NamesMT/15baca1869b72d5c176d2d4fc38a97ec to your computer and use it in GitHub Desktop.

Select an option

Save NamesMT/15baca1869b72d5c176d2d4fc38a97ec to your computer and use it in GitHub Desktop.
Shadcn/vue Button with ripple
<script setup lang="ts">
import { ref, watchEffect, type HTMLAttributes } from "vue";
import { Primitive, type PrimitiveProps } from 'reka-ui'
import { cn } from '@/lib/shadcn/utils'
import { type ButtonVariants, buttonVariants } from '.'
interface Props extends PrimitiveProps {
variant?: ButtonVariants['variant']
size?: ButtonVariants['size']
class?: HTMLAttributes['class']
ripple?: boolean
rippleColorClass?: string;
duration?: number;
}
const props = withDefaults(defineProps<Props>(), {
as: 'button',
rippleColorClass: "bg-accent-foreground/30",
duration: 600,
})
const buttonRef = ref<{$el: HTMLButtonElement} | null>(null);
const buttonRipples = ref<Array<{ x: number; y: number; size: number; key: number }>>([]);
function createRipple(event: MouseEvent) {
const button = buttonRef.value?.$el;
if (!button) return;
const rect = button.getBoundingClientRect();
const size = Math.max(rect.width, rect.height);
const x = event.clientX - rect.left - size / 2;
const y = event.clientY - rect.top - size / 2;
const newRipple = { x, y, size, key: Date.now() };
buttonRipples.value.push(newRipple);
}
watchEffect(() => {
if (buttonRipples.value.length > 0) {
const lastRipple = buttonRipples.value[buttonRipples.value.length - 1]!;
setTimeout(() => {
buttonRipples.value = buttonRipples.value.filter((ripple) => ripple.key !== lastRipple.key);
}, props.duration);
}
});
</script>
<template>
<Primitive
ref="buttonRef"
:as="as"
:as-child="asChild"
:class="cn(buttonVariants({ variant, size }), 'relative overflow-hidden', props.class)"
:style="{ '--duration': $props.duration + 'ms' }"
@pointerdown="$props.ripple && createRipple($event)"
>
<span class="pointer-events-none absolute inset-0">
<span
v-for="ripple in buttonRipples"
:key="ripple.key"
class="ripple-animation absolute rounded-full opacity-30"
:class="[$props.rippleColorClass]"
:style="{
width: ripple.size + 'px',
height: ripple.size + 'px',
top: ripple.y + 'px',
left: ripple.x + 'px',
transform: 'scale(0)',
animationDuration: $props.duration + 'ms',
}"
/>
</span>
<slot />
</Primitive>
</template>
<style scoped>
@keyframes rippling {
0% {
opacity: 1;
}
100% {
transform: scale(2);
opacity: 0;
}
}
.ripple-animation {
animation: rippling var(--duration) ease-out;
}
</style>
@NamesMT
Copy link
Author

NamesMT commented Jul 1, 2025

Credits: shadcn/vue and InspiraUI

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment