Overview
The Balloon component recreates the classic Windows XP system tray notification balloon. It appears from the taskbar with a fade-in animation, plays a notification sound, displays a warning message, and automatically fades out after a duration.
Location: src/components/Balloon/index.jsx
Visual Example
The balloon displays a security warning:
⚠️ Your computer might be at risk
Antivirus software might not be installed
Click this balloon to fix this problem.
Props API
Delay in milliseconds before the balloon appears
How long the balloon stays visible (in milliseconds) before fading out
Usage Example
Basic Usage
import Balloon from 'components/Balloon';
function Footer() {
return (
<div className="footer__items right">
<img src={riskIcon} alt="" />
<div style={{ position: 'relative', width: 0, height: 0 }}>
<Balloon />
</div>
</div>
);
}
Custom Timing
// Appear immediately, stay for 10 seconds
<Balloon startAfter={0} duration={10000} />
// Appear after 5 seconds, stay for 20 seconds
<Balloon startAfter={5000} duration={20000} />
Source: src/WinXP/Footer/index.jsx:139-141
import Balloon from 'components/Balloon';
<div className="footer__items right">
<img className="footer__icon" src={sound} alt="Volume" />
<img className="footer__icon" src={usb} alt="" />
<img className="footer__icon" src={risk} alt="" />
<div style={{ position: 'relative', width: 0, height: 0 }}>
<Balloon />
</div>
<div className="footer__time">{time}</div>
</div>
The balloon is positioned absolutely relative to a zero-size container in the footer.
Component Implementation
State Management
Source: src/components/Balloon/index.jsx:9-20
function Balloon({ startAfter = 3000, duration = 15000 }) {
const [show, setShow] = useState(true);
const [start, setStart] = useState(false);
const audioRef = useRef(null);
const { applyVolume } = useVolume();
// Store applyVolume in a ref to prevent re-running effect
const applyVolumeRef = useRef(applyVolume);
useEffect(() => {
applyVolumeRef.current = applyVolume;
}, [applyVolume]);
}
show: Controls fade in/out animation
start: Controls component mount/unmount
audioRef: Reference to the notification sound Audio object
Timing Logic
Source: src/components/Balloon/index.jsx:31-48
useEffect(() => {
const openTimer = setTimeout(() => {
if (audioRef.current) {
applyVolumeRef.current(audioRef.current);
audioRef.current.play().catch(() => {});
}
setStart(true);
}, startAfter);
const fadeTimer = setTimeout(() => setShow(false), startAfter + duration);
const closeTimer = setTimeout(
() => setStart(false),
startAfter + duration + 1000,
);
return () => {
clearTimeout(openTimer);
clearTimeout(fadeTimer);
clearTimeout(closeTimer);
};
}, [startAfter, duration]);
Timeline:
- 0ms: Component mounts
- startAfter ms: Balloon appears with fade-in, sound plays
- startAfter + duration ms: Fade-out animation begins
- startAfter + duration + 1000ms: Component unmounts
Volume Integration
Source: src/components/Balloon/index.jsx:7, 32-35
import { useVolume } from '../../context/VolumeContext';
const { applyVolume } = useVolume();
if (audioRef.current) {
applyVolumeRef.current(audioRef.current);
audioRef.current.play().catch(() => {});
}
The balloon respects the global volume settings from VolumeContext.
Styled-Components Implementation
Keyframe Animations
Source: src/components/Balloon/index.jsx:79-102
const fadein = keyframes`
0% {
display: block;
opacity: 0;
}
100% {
display: block;
opacity: 1;
}
`;
const fadeout = keyframes`
0% {
display: block;
opacity: 1;
}
99% {
display: block;
opacity: 0;
}
100% {
display: none;
opacity: 0;
}
`;
Styled Container
Source: src/components/Balloon/index.jsx:103-197
Key styling features:
const Div = styled.div`
position: absolute;
display: block;
opacity: 0;
animation: ${({ show }) => (show ? fadein : fadeout)} 1s forwards;
filter: drop-shadow(2px 2px 1px rgba(0, 0, 0, 0.4));
.balloon__container {
position: absolute;
right: -4px;
bottom: 19px;
border: 1px solid black;
border-radius: 7px;
padding: 6px 28px 10px 10px;
background-color: #ffffe1;
font-size: 11px;
white-space: nowrap;
/* Speech bubble pointer using pseudo-elements */
&:before {
content: '';
position: absolute;
bottom: -19px;
right: 14px;
border-style: solid;
border-width: 0 19px 19px 0;
border-color: transparent black transparent transparent;
}
&:after {
content: '';
position: absolute;
bottom: -17px;
right: 15px;
border-style: solid;
border-width: 0 18px 18px 0;
border-color: transparent #ffffe1 transparent transparent;
}
}
`;
Source: src/components/Balloon/index.jsx:153-183
.balloon__close {
outline: none;
position: absolute;
right: 4px;
top: 4px;
width: 14px;
height: 14px;
border: 1px solid rgba(0, 0, 0, 0.1);
border-radius: 3px;
background-color: transparent;
/* X icon using pseudo-elements */
&:before, &:after {
content: '';
position: absolute;
left: 5px;
top: 2px;
height: 8px;
width: 2px;
background-color: rgba(170, 170, 170);
}
&:before { transform: rotate(45deg); }
&:after { transform: rotate(-45deg); }
&:hover {
background-color: #ffa90c;
border-color: white;
&:before, &:after {
background-color: white;
}
}
}
JSX Structure
Source: src/components/Balloon/index.jsx:56-76
return (
start && (
<Div show={show}>
<div className="balloon__container">
<button onClick={() => setShow(false)} className="balloon__close" />
<div className="balloon__header">
<img className="balloon__header__img" src={risk} alt="risk" />
<span className="balloon__header__text">
Your computer might be at risk
</span>
</div>
<p className="balloon__text__first">
Antivirus software might not be installed
</p>
<p className="balloon__text__second">
Click this balloon to fix this problem.
</p>
</div>
</Div>
)
);
Sound Asset
Source: src/components/Balloon/index.jsx:5
import balloonSoundSrc from 'assets/sounds/xp_balloon.wav';
audioRef.current = new Audio(balloonSoundSrc);
The authentic Windows XP balloon notification sound is played when the balloon appears.
Manual Close
Source: src/components/Balloon/index.jsx:60
<button onClick={() => setShow(false)} className="balloon__close" />
Users can manually close the balloon before the auto-fade timeout by clicking the X button.
Positioning Requirements
The balloon requires a positioned parent (relative/absolute) to anchor correctly:
<div style={{ position: 'relative', width: 0, height: 0 }}>
<Balloon />
</div>
The balloon positions itself:
right: -4px
bottom: 19px
- Relative to its parent container
This creates the effect of emerging from the taskbar icon.
Cleanup
Source: src/components/Balloon/index.jsx:45-48
return () => {
clearTimeout(openTimer);
clearTimeout(fadeTimer);
clearTimeout(closeTimer);
if (audioRef.current) {
audioRef.current.pause();
audioRef.current.currentTime = 0;
}
};
All timers and audio are cleaned up when the component unmounts.
Customization Ideas
Custom Content
Modify the component to accept content props:
function Balloon({
startAfter = 3000,
duration = 15000,
title,
message,
icon
}) {
return (
<div className="balloon__header">
<img src={icon} alt="" />
<span>{title}</span>
</div>
<p>{message}</p>
);
}
Click Action
Add an onClick handler:
<div
className="balloon__container"
onClick={() => onBalloonClick?.()}
style={{ cursor: 'pointer' }}
>
See Also