justin.deal/src/components/common/ScrollReveal.astro
Justin Deal b6df2f1a34
All checks were successful
Build and Deploy / build (push) Successful in 39s
Add Ability To Toggle Groups of Homelab Services
2025-05-04 11:36:24 -07:00

123 lines
3.7 KiB
Plaintext

---
interface Props {
animation?: 'fade-up' | 'fade-down' | 'fade-left' | 'fade-right' | 'zoom-in' | 'zoom-out';
duration?: number; // in milliseconds
delay?: number; // in milliseconds
threshold?: number; // 0-1, percentage of element visible to trigger
rootMargin?: string; // CSS margin value
once?: boolean; // animate only once or every time element enters viewport
class?: string;
}
const {
animation = 'fade-up',
duration = 600,
delay = 0,
threshold = 0.1,
rootMargin = '0px',
once = true,
class: className = '',
} = Astro.props;
// Generate a unique ID with better entropy
const id = `scroll-reveal-${crypto.randomUUID().slice(0, 8)}`;
---
<div
id={id}
class:list={['scroll-reveal', `reveal-${animation}`, className]}
data-duration={duration}
data-delay={delay}
data-threshold={threshold}
data-root-margin={rootMargin}
data-once={once}
style="display: contents;"
>
<slot />
</div>
<style>
.scroll-reveal {
opacity: 0;
will-change: transform, opacity;
transition-property: opacity, transform;
}
/* Initial states based on animation type */
.reveal-fade-up { transform: translateY(30px); }
.reveal-fade-down { transform: translateY(-30px); }
.reveal-fade-left { transform: translateX(30px); }
.reveal-fade-right { transform: translateX(-30px); }
.reveal-zoom-in { transform: scale(0.9); }
.reveal-zoom-out { transform: scale(1.1); }
/* Revealed state */
.scroll-reveal.revealed {
opacity: 1;
transform: translate(0) scale(1);
}
@media (prefers-reduced-motion: reduce) {
.scroll-reveal {
transition: none !important;
opacity: 1 !important;
transform: none !important;
}
}
</style>
<script>
// Initialize scroll reveal animations
document.addEventListener('DOMContentLoaded', () => {
const scrollRevealElements = document.querySelectorAll('.scroll-reveal');
if (!scrollRevealElements.length) return;
// Check if IntersectionObserver is supported
if ('IntersectionObserver' in window) {
scrollRevealElements.forEach(element => {
// Get animation parameters from data attributes
const duration = parseInt(element.getAttribute('data-duration') || '600', 10);
const delay = parseInt(element.getAttribute('data-delay') || '0', 10);
const threshold = parseFloat(element.getAttribute('data-threshold') || '0.1');
const rootMargin = element.getAttribute('data-root-margin') || '0px';
const once = element.getAttribute('data-once') === 'true';
// Set transition based on parameters
(element as HTMLElement).style.transition = `opacity ${duration}ms ease ${delay}ms, transform ${duration}ms ease ${delay}ms`;
// Create observer for this element
const observer = new IntersectionObserver(
(entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
entry.target.classList.add('revealed');
// Unobserve if once is true
if (once) {
observer.unobserve(entry.target);
}
} else if (!once) {
// Remove class if element leaves viewport and once is false
entry.target.classList.remove('revealed');
}
});
},
{
threshold,
rootMargin
}
);
// Start observing
observer.observe(element);
});
} else {
// Fallback for browsers that don't support IntersectionObserver
scrollRevealElements.forEach(element => {
element.classList.add('revealed');
});
}
});
</script>