Skip to main content

Scroll Highlight Utilities

The scroll highlight utilities provide a sophisticated system for visually highlighting elements after scroll restoration or for user feedback. Located in src/lib/utils/scroll-highlight.ts, this system offers smooth animations with customizable colors, intensities, and behaviors.

Overview

The scroll highlight system consists of:

  • Core Animation Engine: CSS-based animations with JavaScript control
  • Preset Configurations: Pre-defined styles for common use cases
  • Smart Detection: Automatic element detection and highlighting
  • Performance Optimized: Hardware-accelerated animations with minimal DOM manipulation

Key Features

  • 🎨 Customizable Animations: Multiple colors, intensities, and speeds
  • 🔄 Smooth Transitions: Hardware-accelerated CSS animations
  • 📱 Mobile Optimized: Touch-friendly animations with proper timing
  • Accessibility Aware: Respects user motion preferences
  • 🎯 Smart Targeting: Automatic element detection and boundary snapping
  • 🚀 Performance First: Minimal DOM manipulation and efficient cleanup

Basic Usage

import { applyScrollHighlight } from '@lib/utils/scroll-highlight';

function MyComponent() {
const handleHighlight = () => {
const element = document.getElementById('target-element');
if (element) {
applyScrollHighlight(element);
}
};

return (
<div>
<div id="target-element">Content to highlight</div>
<button onClick={handleHighlight}>Highlight Element</button>
</div>
);
}

Advanced Configuration

import {
applyScrollHighlight,
HIGHLIGHT_PRESETS,
type ScrollHighlightOptions
} from '@lib/utils/scroll-highlight';

function AdvancedHighlight() {
const handleCustomHighlight = () => {
const element = document.querySelector('.my-element');
if (element) {
applyScrollHighlight(element, {
duration: 2000,
intensity: 'intense',
color: 'success',
speed: 'slow',
enablePulse: true,
enableScale: true,
offset: 10
});
}
};

const handlePresetHighlight = () => {
const element = document.querySelector('.preset-element');
if (element) {
applyScrollHighlight(element, HIGHLIGHT_PRESETS.SUCCESS_FAST);
}
};

return (
<div>
<div className="my-element">Custom highlight target</div>
<div className="preset-element">Preset highlight target</div>
<button onClick={handleCustomHighlight}>Custom Highlight</button>
<button onClick={handlePresetHighlight}>Preset Highlight</button>
</div>
);
}

Configuration Options

ScrollHighlightOptions

interface ScrollHighlightOptions {
duration?: number; // Animation duration in milliseconds
offset?: number; // Offset from element edges
className?: string; // Custom CSS class name
intensity?: 'subtle' | 'normal' | 'intense';
color?: 'default' | 'success' | 'warning' | 'error';
speed?: 'instant' | 'fast' | 'normal' | 'slow';
enablePulse?: boolean; // Enable pulsing animation
enableScale?: boolean; // Enable scaling animation
}

Preset Configurations

import { HIGHLIGHT_PRESETS } from '@lib/utils/scroll-highlight';

// Available presets
HIGHLIGHT_PRESETS.DEFAULT; // Standard highlight
HIGHLIGHT_PRESETS.SUCCESS; // Success feedback
HIGHLIGHT_PRESETS.SUCCESS_FAST; // Quick success feedback
HIGHLIGHT_PRESETS.WARNING; // Warning highlight
HIGHLIGHT_PRESETS.ERROR; // Error highlight
HIGHLIGHT_PRESETS.SUBTLE; // Minimal highlight
HIGHLIGHT_PRESETS.INTENSE; // Maximum visibility

Animation Types

Box Shadow Highlight

The primary animation method using CSS box-shadow:

.scroll-restore-highlight {
box-shadow: 0 0 0 4px rgba(var(--highlight-color-rgb), 0.3);
transition: box-shadow var(--highlight-duration) ease-out;
}

Pulse Animation

Optional pulsing effect for enhanced visibility:

.scroll-restore-highlight--pulse {
animation: scroll-highlight-pulse var(--highlight-duration) ease-out;
}

@keyframes scroll-highlight-pulse {
0%,
100% {
box-shadow: 0 0 0 2px rgba(var(--highlight-color-rgb), 0.2);
}
50% {
box-shadow: 0 0 0 8px rgba(var(--highlight-color-rgb), 0.4);
}
}

Scale Animation

Optional scaling effect for dynamic feedback:

.scroll-restore-highlight--scale {
animation: scroll-highlight-scale var(--highlight-duration) ease-out;
}

@keyframes scroll-highlight-scale {
0% {
transform: scale(1);
}
50% {
transform: scale(var(--highlight-scale-max, 1.02));
}
100% {
transform: scale(1);
}
}

Color System

CSS Custom Properties

The system uses CSS custom properties for theming:

:root {
--scroll-highlight-default: #007bff;
--scroll-highlight-default-rgb: 0, 123, 255;
--scroll-highlight-success: #28a745;
--scroll-highlight-success-rgb: 40, 167, 69;
--scroll-highlight-warning: #ffc107;
--scroll-highlight-warning-rgb: 255, 193, 7;
--scroll-highlight-error: #dc3545;
--scroll-highlight-error-rgb: 220, 53, 69;
}

Dynamic Color Application

Colors are applied dynamically based on configuration:

function applyHighlightColor(element: HTMLElement, color: string) {
const colorMap = {
default: 'var(--scroll-highlight-default-rgb)',
success: 'var(--scroll-highlight-success-rgb)',
warning: 'var(--scroll-highlight-warning-rgb)',
error: 'var(--scroll-highlight-error-rgb)'
};

element.style.setProperty('--highlight-color-rgb', colorMap[color]);
}

Performance Considerations

Hardware Acceleration

All animations use hardware-accelerated CSS properties:

.scroll-restore-highlight {
will-change: box-shadow, transform;
backface-visibility: hidden;
perspective: 1000px;
}

Efficient Cleanup

Automatic cleanup prevents memory leaks:

function cleanupHighlight(element: HTMLElement, duration: number) {
setTimeout(() => {
element.classList.remove('scroll-restore-highlight');
element.style.removeProperty('--highlight-duration');
element.style.removeProperty('--highlight-color-rgb');
element.style.removeProperty('will-change');
}, duration + 100);
}

Debounced Application

Multiple highlights on the same element are debounced:

const highlightTimers = new WeakMap<HTMLElement, number>();

function applyScrollHighlight(
element: HTMLElement,
options: ScrollHighlightOptions
) {
// Clear existing timer
const existingTimer = highlightTimers.get(element);
if (existingTimer) {
clearTimeout(existingTimer);
}

// Apply new highlight
// ... highlight logic

// Set cleanup timer
const timer = setTimeout(() => cleanup(element), duration);
highlightTimers.set(element, timer);
}

Accessibility

Motion Preferences

Respects user motion preferences:

function shouldUseAnimation(): boolean {
if (typeof window === 'undefined') return false;

const mediaQuery = window.matchMedia('(prefers-reduced-motion: reduce)');
return !mediaQuery.matches;
}

Screen Reader Support

Provides appropriate ARIA announcements:

function announceHighlight(element: HTMLElement, type: string) {
const announcement = `${type} highlight applied to ${element.tagName.toLowerCase()}`;

// Create temporary announcement element
const announcer = document.createElement('div');
announcer.setAttribute('aria-live', 'polite');
announcer.setAttribute('aria-atomic', 'true');
announcer.className = 'sr-only';
announcer.textContent = announcement;

document.body.appendChild(announcer);
setTimeout(() => document.body.removeChild(announcer), 1000);
}

Integration Examples

Scroll Restoration

Highlight elements after scroll restoration:

import { applyScrollHighlight } from '@lib/utils/scroll-highlight';

function useScrollRestoration() {
const highlightRestoredElement = (elementId: string) => {
const element = document.getElementById(elementId);
if (element) {
// Scroll to element
element.scrollIntoView({ behavior: 'smooth', block: 'center' });

// Highlight after scroll
setTimeout(() => {
applyScrollHighlight(element, {
color: 'default',
intensity: 'normal',
duration: 1500
});
}, 500);
}
};

return { highlightRestoredElement };
}

Form Validation

Highlight form fields with validation errors:

function FormField({ error, children }) {
const fieldRef = useRef<HTMLDivElement>(null);

useEffect(() => {
if (error && fieldRef.current) {
applyScrollHighlight(fieldRef.current, {
color: 'error',
intensity: 'subtle',
duration: 1000,
enablePulse: true
});
}
}, [error]);

return (
<div ref={fieldRef} className={error ? 'field-error' : ''}>
{children}
</div>
);
}

Success Feedback

Highlight successful actions:

function SuccessButton() {
const buttonRef = useRef<HTMLButtonElement>(null);

const handleSuccess = () => {
if (buttonRef.current) {
applyScrollHighlight(buttonRef.current, {
color: 'success',
intensity: 'normal',
duration: 1200,
enableScale: true
});
}
};

return (
<button ref={buttonRef} onClick={handleSuccess}>
Save Changes
</button>
);
}

Browser Support

  • Modern Browsers: Chrome 90+, Firefox 88+, Safari 14+, Edge 90+
  • CSS Features: CSS Custom Properties, CSS Animations, box-shadow
  • JavaScript Features: ES2020+, WeakMap, setTimeout/clearTimeout
  • Fallback: Graceful degradation with no animation

Testing

Unit Tests

import { applyScrollHighlight } from '@lib/utils/scroll-highlight';

describe('ScrollHighlight', () => {
test('applies highlight class to element', () => {
const element = document.createElement('div');
applyScrollHighlight(element);

expect(element.classList.contains('scroll-restore-highlight')).toBe(true);
});

test('respects custom duration', () => {
const element = document.createElement('div');
applyScrollHighlight(element, { duration: 2000 });

expect(element.style.getPropertyValue('--highlight-duration')).toBe(
'2000ms'
);
});
});

Integration Tests

test('highlights element after scroll restoration', async () => {
const { getByTestId } = render(<MyComponent />);
const element = getByTestId('highlight-target');

// Trigger scroll restoration
fireEvent.click(getByTestId('restore-button'));

// Wait for highlight to apply
await waitFor(() => {
expect(element).toHaveClass('scroll-restore-highlight');
});
});
  • Scroll Position: Scroll position management and restoration
  • Smooth Scrolling: Enhanced scrolling with easing functions
  • Viewport Detection: Element visibility and intersection detection
  • Animation Utils: General animation utilities and helpers