Skip to main content

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

startAfter
number
default:"3000"
Delay in milliseconds before the balloon appears
duration
number
default:"15000"
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:
  1. 0ms: Component mounts
  2. startAfter ms: Balloon appears with fade-in, sound plays
  3. startAfter + duration ms: Fade-out animation begins
  4. 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;
    }
  }
`;

Close Button Styling

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

Build docs developers (and LLMs) love