// ============================================================================
// Stylescape | Content Revealer
// ============================================================================
// Reveals content with fade-in effects after page load.
// Supports data-ss-reveal attributes for declarative configuration.
// ============================================================================
/**
* Configuration options for ContentRevealer
*/
export interface ContentRevealerOptions {
/** Delay before revealing (ms) */
delay?: number;
/** Duration of fade animation (ms) */
duration?: number;
/** Easing function */
easing?: string;
/** Initial opacity */
initialOpacity?: number;
/** Whether to use IntersectionObserver for scroll-triggered reveal */
onScroll?: boolean;
/** Threshold for IntersectionObserver (0-1) */
threshold?: number;
}
/**
* Reveals content with a fade-in effect.
*
* @example JavaScript
* ```typescript
* const revealer = new ContentRevealer(".hidden-content", { delay: 500 })
* ```
*
* @example HTML with data-ss
* ```html
*
* Content to reveal
*
* ```
*/
export class ContentRevealer {
private elements: HTMLElement[];
private options: Required;
private observer: IntersectionObserver | null = null;
constructor(
selectorOrElements:
| string
| HTMLElement
| HTMLElement[]
| NodeListOf,
options: ContentRevealerOptions = {},
) {
// Normalize input to array
if (typeof selectorOrElements === "string") {
this.elements = Array.from(
document.querySelectorAll(selectorOrElements),
);
} else if (selectorOrElements instanceof HTMLElement) {
this.elements = [selectorOrElements];
} else {
this.elements = Array.from(selectorOrElements);
}
this.options = {
delay: options.delay ?? 0,
duration: options.duration ?? 500,
easing: options.easing ?? "ease",
initialOpacity: options.initialOpacity ?? 0,
onScroll: options.onScroll ?? false,
threshold: options.threshold ?? 0.1,
};
this.init();
}
// ========================================================================
// Public Methods
// ========================================================================
/**
* Manually reveal all elements
*/
public revealAll(): void {
this.elements.forEach((el) => this.reveal(el));
}
/**
* Reveal a specific element
*/
public reveal(element: HTMLElement): void {
element.style.transition = `opacity ${this.options.duration}ms ${this.options.easing}`;
element.style.opacity = "1";
element.classList.add("reveal--visible");
element.setAttribute("data-ss-reveal-revealed", "true");
}
/**
* Reset element to hidden state
*/
public hide(element: HTMLElement): void {
element.style.opacity = String(this.options.initialOpacity);
element.classList.remove("reveal--visible");
element.removeAttribute("data-ss-reveal-revealed");
}
/**
* Reset all elements to hidden state
*/
public hideAll(): void {
this.elements.forEach((el) => this.hide(el));
}
/**
* Destroy the revealer instance
*/
public destroy(): void {
if (this.observer) {
this.observer.disconnect();
this.observer = null;
}
}
// ========================================================================
// Private Methods
// ========================================================================
private init(): void {
// Set initial hidden state
this.elements.forEach((el) => {
el.style.opacity = String(this.options.initialOpacity);
el.style.transition = `opacity ${this.options.duration}ms ${this.options.easing}`;
});
if (this.options.onScroll) {
this.initIntersectionObserver();
} else {
this.initLoadReveal();
}
}
private initLoadReveal(): void {
const reveal = () => {
setTimeout(() => this.revealAll(), this.options.delay);
};
if (document.readyState === "complete") {
reveal();
} else {
window.addEventListener("load", reveal);
}
}
private initIntersectionObserver(): void {
this.observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
const el = entry.target as HTMLElement;
setTimeout(() => this.reveal(el), this.options.delay);
this.observer?.unobserve(el);
}
});
},
{ threshold: this.options.threshold },
);
this.elements.forEach((el) => this.observer?.observe(el));
}
}
export default ContentRevealer;