Buttons buttonrippleinteractivemicro-interaction
Ripple Button
A sleek interactive button that triggers localized expanding ripple rings on click.
Preview
Usage
Copy the full block below to use this component with its imports.
astro
---
import { RippleButton } from 'astro-component-kit';
---
<RippleButton variant="primary" size="md">Click Ripple</RippleButton> ---
import { RippleButton } from 'astro-component-kit';
---
<RippleButton variant="primary" size="md">Click Ripple</RippleButton> Manual Installation
If you are not using the npm package, create a new file src/components/lib/RippleButton.astro and paste the following code:
astro
---
/**
* RippleButton — Interactive button featuring an expanding click ripple effect with glass styling.
*
* @param {'primary' | 'secondary' | 'ghost'} variant - Visual style variant. Default is 'primary'.
* @param {'sm' | 'md' | 'lg'} size - Button size variant. Default is 'md'.
* @param {string} href - Optional link destination.
* @param {boolean} disabled - Whether the button is disabled.
* @param {string} className - Additional CSS class names.
*/
interface Props {
variant?: 'primary' | 'secondary' | 'ghost';
size?: 'sm' | 'md' | 'lg';
href?: string;
disabled?: boolean;
className?: string;
}
const {
variant = 'primary',
size = 'md',
href,
disabled = false,
className = '',
} = Astro.props;
const Tag = href ? 'a' : 'button';
---
<Tag
href={href}
disabled={!href && disabled ? true : undefined}
aria-disabled={disabled ? 'true' : undefined}
class:list={['ripple-btn', `ripple-btn--${variant}`, `ripple-btn--${size}`, className]}
data-ripple-btn
>
<span class="ripple-btn__label"><slot /></span>
<span class="ripple-btn__container" aria-hidden="true"></span>
</Tag>
<style>
.ripple-btn {
--btn-primary-bg: linear-gradient(135deg, rgba(184, 134, 11, 0.9), rgba(212, 160, 23, 0.9));
--btn-primary-color: #050505;
--btn-secondary-bg: rgba(255, 255, 255, 0.05);
--btn-secondary-color: var(--c-text-1, #ffffff);
--btn-ghost-bg: transparent;
--btn-ghost-color: var(--c-primary-light, #d4a017);
position: relative;
display: inline-flex;
align-items: center;
justify-content: center;
overflow: hidden;
font-family: var(--font-sans, system-ui, sans-serif);
font-weight: 700;
text-decoration: none;
border-radius: var(--r-md, 12px);
border: 1px solid rgba(255, 255, 255, 0.1);
cursor: pointer;
user-select: none;
transition: transform 0.2s cubic-bezier(0.16, 1, 0.3, 1), box-shadow 0.2s ease, border-color 0.2s ease;
}
.ripple-btn:focus-visible {
outline: 2px solid var(--c-primary-light, #d4a017);
outline-offset: 3px;
}
.ripple-btn[disabled],
.ripple-btn[aria-disabled="true"] {
opacity: 0.5;
cursor: not-allowed;
pointer-events: none;
}
/* Variants */
.ripple-btn--primary {
background: var(--btn-primary-bg);
color: var(--btn-primary-color);
box-shadow: 0 4px 20px rgba(184, 134, 11, 0.25);
}
.ripple-btn--primary:hover {
transform: translateY(-2px);
box-shadow: 0 8px 30px rgba(184, 134, 11, 0.4);
border-color: rgba(254, 243, 199, 0.4);
}
.ripple-btn--secondary {
background: var(--btn-secondary-bg);
color: var(--btn-secondary-color);
backdrop-filter: blur(12px);
}
.ripple-btn--secondary:hover {
background: rgba(255, 255, 255, 0.09);
border-color: rgba(184, 134, 11, 0.3);
transform: translateY(-2px);
}
.ripple-btn--ghost {
background: var(--btn-ghost-bg);
color: var(--btn-ghost-color);
border-color: transparent;
}
.ripple-btn--ghost:hover {
background: rgba(184, 134, 11, 0.1);
border-color: rgba(184, 134, 11, 0.2);
}
/* Sizes */
.ripple-btn--sm {
padding: 0.45rem 1rem;
font-size: 0.8rem;
letter-spacing: 0.02em;
}
.ripple-btn--md {
padding: 0.75rem 1.6rem;
font-size: 0.92rem;
letter-spacing: 0.02em;
}
.ripple-btn--lg {
padding: 1rem 2.2rem;
font-size: 1.05rem;
letter-spacing: 0.03em;
}
.ripple-btn__label {
position: relative;
z-index: 2;
display: inline-flex;
align-items: center;
gap: var(--sp-2, 0.5rem);
pointer-events: none;
}
.ripple-btn__container {
position: absolute;
inset: 0;
z-index: 1;
overflow: hidden;
pointer-events: none;
}
:global(.ripple-effect) {
position: absolute;
border-radius: 50%;
transform: scale(0);
animation: ripple-grow 0.6s cubic-bezier(0, 0, 0.2, 1) forwards;
background: rgba(255, 255, 255, 0.35);
pointer-events: none;
}
@keyframes ripple-grow {
to {
transform: scale(4);
opacity: 0;
}
}
@media (prefers-reduced-motion: reduce) {
.ripple-btn { transition: none; }
:global(.ripple-effect) { display: none; }
}
</style>
<script>
function setupRipples() {
const buttons = document.querySelectorAll('[data-ripple-btn]');
buttons.forEach((btn) => {
if (btn.getAttribute('data-ripple-init')) return;
btn.setAttribute('data-ripple-init', 'true');
btn.addEventListener('pointerdown', (e: Event) => {
const mouseEvent = e as MouseEvent;
const target = mouseEvent.currentTarget as HTMLElement;
const container = target.querySelector('.ripple-btn__container');
if (!container) return;
const rect = target.getBoundingClientRect();
const size = Math.max(rect.width, rect.height);
const x = mouseEvent.clientX - rect.left - size / 2;
const y = mouseEvent.clientY - rect.top - size / 2;
const ripple = document.createElement('span');
ripple.className = 'ripple-effect';
ripple.style.width = `${size}px`;
ripple.style.height = `${size}px`;
ripple.style.left = `${x}px`;
ripple.style.top = `${y}px`;
container.appendChild(ripple);
setTimeout(() => ripple.remove(), 650);
});
});
}
setupRipples();
document.addEventListener('astro:page-load', setupRipples);
</script> ---
/**
* RippleButton — Interactive button featuring an expanding click ripple effect with glass styling.
*
* @param {'primary' | 'secondary' | 'ghost'} variant - Visual style variant. Default is 'primary'.
* @param {'sm' | 'md' | 'lg'} size - Button size variant. Default is 'md'.
* @param {string} href - Optional link destination.
* @param {boolean} disabled - Whether the button is disabled.
* @param {string} className - Additional CSS class names.
*/
interface Props {
variant?: 'primary' | 'secondary' | 'ghost';
size?: 'sm' | 'md' | 'lg';
href?: string;
disabled?: boolean;
className?: string;
}
const {
variant = 'primary',
size = 'md',
href,
disabled = false,
className = '',
} = Astro.props;
const Tag = href ? 'a' : 'button';
---
<Tag
href={href}
disabled={!href && disabled ? true : undefined}
aria-disabled={disabled ? 'true' : undefined}
class:list={['ripple-btn', `ripple-btn--${variant}`, `ripple-btn--${size}`, className]}
data-ripple-btn
>
<span class="ripple-btn__label"><slot /></span>
<span class="ripple-btn__container" aria-hidden="true"></span>
</Tag>
<style>
.ripple-btn {
--btn-primary-bg: linear-gradient(135deg, rgba(184, 134, 11, 0.9), rgba(212, 160, 23, 0.9));
--btn-primary-color: #050505;
--btn-secondary-bg: rgba(255, 255, 255, 0.05);
--btn-secondary-color: var(--c-text-1, #ffffff);
--btn-ghost-bg: transparent;
--btn-ghost-color: var(--c-primary-light, #d4a017);
position: relative;
display: inline-flex;
align-items: center;
justify-content: center;
overflow: hidden;
font-family: var(--font-sans, system-ui, sans-serif);
font-weight: 700;
text-decoration: none;
border-radius: var(--r-md, 12px);
border: 1px solid rgba(255, 255, 255, 0.1);
cursor: pointer;
user-select: none;
transition: transform 0.2s cubic-bezier(0.16, 1, 0.3, 1), box-shadow 0.2s ease, border-color 0.2s ease;
}
.ripple-btn:focus-visible {
outline: 2px solid var(--c-primary-light, #d4a017);
outline-offset: 3px;
}
.ripple-btn[disabled],
.ripple-btn[aria-disabled="true"] {
opacity: 0.5;
cursor: not-allowed;
pointer-events: none;
}
/* Variants */
.ripple-btn--primary {
background: var(--btn-primary-bg);
color: var(--btn-primary-color);
box-shadow: 0 4px 20px rgba(184, 134, 11, 0.25);
}
.ripple-btn--primary:hover {
transform: translateY(-2px);
box-shadow: 0 8px 30px rgba(184, 134, 11, 0.4);
border-color: rgba(254, 243, 199, 0.4);
}
.ripple-btn--secondary {
background: var(--btn-secondary-bg);
color: var(--btn-secondary-color);
backdrop-filter: blur(12px);
}
.ripple-btn--secondary:hover {
background: rgba(255, 255, 255, 0.09);
border-color: rgba(184, 134, 11, 0.3);
transform: translateY(-2px);
}
.ripple-btn--ghost {
background: var(--btn-ghost-bg);
color: var(--btn-ghost-color);
border-color: transparent;
}
.ripple-btn--ghost:hover {
background: rgba(184, 134, 11, 0.1);
border-color: rgba(184, 134, 11, 0.2);
}
/* Sizes */
.ripple-btn--sm {
padding: 0.45rem 1rem;
font-size: 0.8rem;
letter-spacing: 0.02em;
}
.ripple-btn--md {
padding: 0.75rem 1.6rem;
font-size: 0.92rem;
letter-spacing: 0.02em;
}
.ripple-btn--lg {
padding: 1rem 2.2rem;
font-size: 1.05rem;
letter-spacing: 0.03em;
}
.ripple-btn__label {
position: relative;
z-index: 2;
display: inline-flex;
align-items: center;
gap: var(--sp-2, 0.5rem);
pointer-events: none;
}
.ripple-btn__container {
position: absolute;
inset: 0;
z-index: 1;
overflow: hidden;
pointer-events: none;
}
:global(.ripple-effect) {
position: absolute;
border-radius: 50%;
transform: scale(0);
animation: ripple-grow 0.6s cubic-bezier(0, 0, 0.2, 1) forwards;
background: rgba(255, 255, 255, 0.35);
pointer-events: none;
}
@keyframes ripple-grow {
to {
transform: scale(4);
opacity: 0;
}
}
@media (prefers-reduced-motion: reduce) {
.ripple-btn { transition: none; }
:global(.ripple-effect) { display: none; }
}
</style>
<script>
function setupRipples() {
const buttons = document.querySelectorAll('[data-ripple-btn]');
buttons.forEach((btn) => {
if (btn.getAttribute('data-ripple-init')) return;
btn.setAttribute('data-ripple-init', 'true');
btn.addEventListener('pointerdown', (e: Event) => {
const mouseEvent = e as MouseEvent;
const target = mouseEvent.currentTarget as HTMLElement;
const container = target.querySelector('.ripple-btn__container');
if (!container) return;
const rect = target.getBoundingClientRect();
const size = Math.max(rect.width, rect.height);
const x = mouseEvent.clientX - rect.left - size / 2;
const y = mouseEvent.clientY - rect.top - size / 2;
const ripple = document.createElement('span');
ripple.className = 'ripple-effect';
ripple.style.width = `${size}px`;
ripple.style.height = `${size}px`;
ripple.style.left = `${x}px`;
ripple.style.top = `${y}px`;
container.appendChild(ripple);
setTimeout(() => ripple.remove(), 650);
});
});
}
setupRipples();
document.addEventListener('astro:page-load', setupRipples);
</script>
Quick Info
- Category
- Buttons
- Filename
RippleButton.astro- Dependencies
- None — pure Astro + CSS
- Tags
- buttonrippleinteractivemicro-interaction