This document details the StarsBackground component that creates an animated starfield background effect.
Location: src/components/Stars/StarsBackground.tsx
The StarsBackground component creates a visually appealing animated background with twinkling stars and occasional shooting stars.
flowchart TD
StarsBackground[StarsBackground] -->|Creates| Container[Sky Container]
Container -->|Generates| Stars[Star Elements]
Stars -->|Random| Position[Random Positions]
Stars -->|Random| Size[Random Sizes]
Stars -->|Random| Animation[Twinkle Animation]
Stars -->|Occasional| Shooting[Shooting Stars]
Stars are generated on component mount based on window width:
useEffect(() => {
const maxStars = typeof window !== 'undefined' && window?.innerWidth ? window?.innerWidth : 400;
const numberOfStars = Math.floor(Math.random() * (maxStars / 2)) + 10;
const starsArray: ReactElement[] = [];
for (let i = 0; i < numberOfStars; i += 1) {
const starSize = `${Math.random() * 5 + 1}px`;
const style = {
background: `#ffffff50`,
borderRadius: '50%',
opacity: 0.5,
position: 'absolute',
transition: 'transform 1s',
animation: `twinkle ${Math.random() * 5}s ease-in-out infinite`,
width: starSize,
height: starSize,
top: `${Math.random() * 100}vh`,
left: `${Math.random() * 100}vw`,
};
starsArray.push(<Box key={i} data-testid='star' sx={style} onMouseEnter={handleStarAnimation} />);
}
setStars(starsArray);
}, []);
Star Count: Based on window width (10 to width/2 stars)
Each star has:
vh, vw) for consistent sizing#ffffff50)Stars twinkle using CSS animations applied through the sx prop. Each star receives:
twinkle with dynamic duration (using template literal with star.animationDuration) set to infinitestar.animationDelay for staggered effectThe twinkle keyframe animation should be defined in global styles:
@keyframes twinkle {
0%,
100% {
opacity: 1;
}
50% {
opacity: 0.3;
}
}
Stars can become shooting stars on hover or through automatic triggering:
import { THRESHOLDS } from '@constants/index';
const handleStarAnimation = (e: React.MouseEvent<HTMLElement> | { target: HTMLElement }): void => {
const target = e.target as HTMLElement;
const shootingStarSpeed = Math.random() * 4 + 1;
target.style.animation = `shootAway ${shootingStarSpeed}s forwards`;
target.style.background = '#fff90050';
target.style.transform = `scale(${Math.random() * 2 + 1})`;
setTimeout(() => {
if (target) {
target.setAttribute('data-star-used', 'true');
}
}, shootingStarSpeed * 1000);
};
Automatic Shooting Stars:
If there are more than THRESHOLDS.MIN_STARS_FOR_ANIMATION (15) unused stars, the component automatically triggers random shooting star animations:
const handleForceStarAnimation = () => {
const allStars = Array.from(document.querySelectorAll('[data-testid="star"]')).filter(
(star) => star.getAttribute('data-star-used') !== 'true',
);
if (!isEmpty(allStars) && allStars.length > THRESHOLDS.MIN_STARS_FOR_ANIMATION) {
const randomStar = allStars[Math.floor(Math.random() * allStars.length)] as HTMLElement;
if (randomStar) {
handleStarAnimation({ target: randomStar });
}
const randomTime = Math.random() * 5 + 1.5;
forceAnimationTimeoutRef.current = setTimeout(() => {
handleForceStarAnimation();
}, randomTime * 1000);
}
};
sequenceDiagram
participant Component
participant State
participant DOM
participant CSS
Component->>Component: Mount
Component->>State: Calculate star count (50-100)
Component->>State: Generate star properties
State-->>Component: Update stars array
Component->>DOM: Render star elements
DOM->>CSS: Apply animations
CSS->>DOM: Animate stars
The component uses proper ARIA attributes for screen readers:
id='sky' - Unique identifier for the containeraria-label='Starry background' - Descriptive label for assistive technologiescomponent='div' - Renders as a div elementrole='img' - Identifies as an image for screen readerssx prop contains styling with spread operator for dynamic stylesNote: Unlike decorative backgrounds that use aria-hidden='true', this component uses role='img' with an aria-label because it’s a significant visual element of the user experience.
position: fixed to avoid reflowoverflow: hidden prevents scrollbarsCleanup Logic:
useEffect(() => {
createStars();
// Cleanup timeout on unmount to prevent memory leaks
return () => {
if (forceAnimationTimeoutRef.current) {
clearTimeout(forceAnimationTimeoutRef.current);
}
};
}, []);
The component is rendered in GeneralLayout:
export default function GeneralLayout({ children }) {
return (
<div id='content'>
<Navbar />
<main>
{children}
<StarsBackground />
<CookieSnackbar />
</main>
<Footer />
</div>
);
}
Test file: src/components/Stars/StarsBackground.test.tsx
Test Coverage:
To customize the background:
Math.floor(Math.random() * (maxStars / 2)) + 10 in createStars()Math.random() * 5 + 1 (currently 1-6px)Math.random() * 5 for twinkle durationMath.random() * 4 + 1 in handleStarAnimationbody background (#131518)background: '#ffffff50' in starStylesTHRESHOLDS.MIN_STARS_FOR_ANIMATION in constantsDELAYS.STAR_ANIMATION_INITIAL in constantsstateDiagram-v2
[*] --> Visible: opacity 1
Visible --> Fading: 2-5s random
Fading --> Dim: opacity 0.3
Dim --> Brightening: 2-5s random
Brightening --> Visible: opacity 1
💡 Tip: The starfield creates depth and visual interest without distracting from content. Keep star count reasonable for performance.