Skip to main content

Overview

The VolumeSlider component recreates the classic Windows XP volume control popup that appears when clicking the speaker icon in the system tray. It features a vertical slider, mute checkbox, and authentic XP styling. Location: src/components/VolumeSlider/index.jsx

Props API

volume
number
required
Current volume level (0-100)
onVolumeChange
function
required
Callback when volume slider changes
(newVolume: number) => void
isMuted
boolean
required
Current mute state
onMuteChange
function
required
Callback when mute checkbox is toggled
(muted: boolean) => void

Usage Example

Basic Implementation

import { useState } from 'react';
import VolumeSlider from 'components/VolumeSlider';

function MyApp() {
  const [volume, setVolume] = useState(50);
  const [isMuted, setIsMuted] = useState(false);

  return (
    <VolumeSlider
      volume={volume}
      onVolumeChange={setVolume}
      isMuted={isMuted}
      onMuteChange={setIsMuted}
    />
  );
}
Source: src/WinXP/Footer/index.jsx:43-46, 145-154
import { useState, useRef, useEffect } from 'react';
import VolumeSlider from '../../components/VolumeSlider';
import { useVolume } from '../../context/VolumeContext';

function Footer() {
  const [showVolume, setShowVolume] = useState(false);
  const { volume, setVolume, isMuted, setIsMuted } = useVolume();
  const sliderRef = useRef(null);
  const soundIconRef = useRef(null);

  // Click-outside detection
  useEffect(() => {
    function handleClickOutside(event) {
      if (
        showVolume &&
        sliderRef.current &&
        !sliderRef.current.contains(event.target) &&
        soundIconRef.current &&
        !soundIconRef.current.contains(event.target)
      ) {
        setShowVolume(false);
      }
    }
    document.addEventListener('mousedown', handleClickOutside);
    return () => document.removeEventListener('mousedown', handleClickOutside);
  }, [showVolume]);

  return (
    <>
      <img
        ref={soundIconRef}
        className="footer__icon"
        src={sound}
        alt="Volume"
        onClick={() => setShowVolume(v => !v)}
        style={{ cursor: 'pointer' }}
      />
      
      <div ref={sliderRef}>
        {showVolume && (
          <VolumeSlider
            volume={volume}
            onVolumeChange={setVolume}
            isMuted={isMuted}
            onMuteChange={setIsMuted}
          />
        )}
      </div>
    </>
  );
}

Component Implementation

Event Handlers

Source: src/components/VolumeSlider/index.jsx:69-80
function VolumeSlider({ volume, onVolumeChange, isMuted, onMuteChange }) {
  const handleSliderChange = e => {
    onVolumeChange(Number(e.target.value));
    // Automatically unmute when user drags slider
    if (isMuted) {
      onMuteChange(false);
    }
  };

  const handleCheckboxChange = e => {
    onMuteChange(e.target.checked);
  };
}
Behavior:
  • Dragging the slider automatically unmutes
  • Mute checkbox toggles mute state
  • When muted, slider shows 0 but preserves volume value

JSX Structure

Source: src/components/VolumeSlider/index.jsx:82-101
return (
  <SliderWrapper>
    <Title>Volume</Title>
    <Slider
      type="range"
      min="0"
      max="100"
      value={isMuted ? 0 : volume}
      onChange={handleSliderChange}
    />
    <MuteContainer>
      <input
        type="checkbox"
        checked={isMuted}
        onChange={handleCheckboxChange}
      />
      Mute
    </MuteContainer>
  </SliderWrapper>
);

Styled-Components Implementation

Wrapper Container

Source: src/components/VolumeSlider/index.jsx:5-20
const SliderWrapper = styled.div`
  position: absolute;
  bottom: 32px;           /* Positioned above the 30px footer */
  right: 60px;            /* Aligned near the sound icon */
  background: #f0f0f0;
  border: 1px solid #808080;
  border-top-color: #fff;
  border-left-color: #fff;
  box-shadow: 1px 1px 1px #000;
  padding: 10px;
  padding-bottom: 5px;
  z-index: 10000;
  font-family: 'Tahoma', sans-serif;
  font-size: 11px;
  color: #000;
`;
Key features:
  • Absolutely positioned above footer
  • Classic Windows XP beveled border (light top/left, dark bottom/right)
  • High z-index to appear above other elements

Title Styling

Source: src/components/VolumeSlider/index.jsx:22-25
const Title = styled.div`
  margin-bottom: 8px;
  padding-left: 2px;
`;

Vertical Slider

Source: src/components/VolumeSlider/index.jsx:27-34
const Slider = styled.input`
  -webkit-appearance: slider-vertical;
  width: 20px;
  height: 100px;
  margin: 0 auto;
  display: block;
`;
Important: The -webkit-appearance: slider-vertical property creates the vertical slider orientation.

Mute Checkbox Container

Source: src/components/VolumeSlider/index.jsx:36-67
const MuteContainer = styled.label`
  display: flex;
  align-items: center;
  margin-top: 8px;
  cursor: pointer;

  input {
    margin-right: 5px;
    -webkit-appearance: none;
    -moz-appearance: none;
    appearance: none;
    width: 13px;
    height: 13px;
    background: #fff;
    border: 1px solid #808080;
    box-shadow: inset 1px 1px #000;
    position: relative;
    cursor: pointer;

    &:checked::after {
      content: '✓';
      font-size: 12px;
      font-weight: bold;
      color: #000;
      position: absolute;
      top: -2px;
      left: 1px;
    }
  }
`;
Features:
  • Custom checkbox styling (removes browser defaults)
  • Inset shadow for 3D effect
  • Checkmark (✓) appears when checked

Volume Context Integration

The component typically integrates with a global VolumeContext: Context Structure:
// context/VolumeContext.jsx
import { createContext, useContext, useState } from 'react';

const VolumeContext = createContext();

export function VolumeProvider({ children }) {
  const [volume, setVolume] = useState(50);
  const [isMuted, setIsMuted] = useState(false);

  const applyVolume = (audioElement) => {
    if (isMuted) {
      audioElement.volume = 0;
    } else {
      audioElement.volume = volume / 100;
    }
  };

  return (
    <VolumeContext.Provider value={{ 
      volume, 
      setVolume, 
      isMuted, 
      setIsMuted, 
      applyVolume 
    }}>
      {children}
    </VolumeContext.Provider>
  );
}

export const useVolume = () => useContext(VolumeContext);
Usage with Context:
import { useVolume } from 'context/VolumeContext';

function Footer() {
  const { volume, setVolume, isMuted, setIsMuted } = useVolume();
  
  return (
    <VolumeSlider
      volume={volume}
      onVolumeChange={setVolume}
      isMuted={isMuted}
      onMuteChange={setIsMuted}
    />
  );
}

Click-Outside Pattern

The Footer implements click-outside detection to close the volume slider: Source: src/WinXP/Footer/index.jsx:82-99
const sliderRef = useRef(null);
const soundIconRef = useRef(null);

useEffect(() => {
  function handleClickOutside(event) {
    // Close if clicking outside BOTH the slider AND the sound icon
    if (
      showVolume &&
      sliderRef.current &&
      !sliderRef.current.contains(event.target) &&
      soundIconRef.current &&
      !soundIconRef.current.contains(event.target)
    ) {
      setShowVolume(false);
    }
  }
  
  document.addEventListener('mousedown', handleClickOutside);
  return () => document.removeEventListener('mousedown', handleClickOutside);
}, [showVolume]);
Why check both refs?
  • Clicking outside the slider should close it
  • Clicking the sound icon should toggle it (not close immediately)

Positioning Strategy

The component uses absolute positioning relative to the footer:
position: absolute;
bottom: 32px;   /* 30px footer + 2px spacing */
right: 60px;    /* Approximate alignment with sound icon */
Layout Structure:
┌─────────────────────────────────────┐
│                                     │
│         Desktop Area                │
│                                     │
│                         ┌──────┐    │
│                         │Volume│    │ ← Slider popup
│                         │  ║   │    │
│                         │ Mute │    │
│                         └──────┘    │
├─────────────────────────────────────┤
│ [Start] [Apps...]    🔊 📱 ⚠️ 12:00│ ← Footer (30px)
└─────────────────────────────────────┘

Auto-Unmute Behavior

Source: src/components/VolumeSlider/index.jsx:72-75
const handleSliderChange = e => {
  onVolumeChange(Number(e.target.value));
  if (isMuted) {
    onMuteChange(false);  // Auto-unmute when dragging slider
  }
};
This matches Windows XP behavior:
  1. User mutes audio
  2. User drags volume slider
  3. Audio automatically unmutes at the new volume level

Applying Volume to Audio Elements

Example from Balloon component: Source: src/components/Balloon/index.jsx:32-35
const { applyVolume } = useVolume();

if (audioRef.current) {
  applyVolume(audioRef.current);  // Sets audio.volume based on slider
  audioRef.current.play().catch(() => {});
}
applyVolume implementation:
const applyVolume = (audioElement) => {
  if (isMuted) {
    audioElement.volume = 0;
  } else {
    audioElement.volume = volume / 100;  // Convert 0-100 to 0-1
  }
};

Browser Compatibility

Vertical Slider Support

-webkit-appearance: slider-vertical;
Support:
  • ✅ Chrome/Edge
  • ✅ Safari
  • ⚠️ Firefox (requires writing-mode or transform)
Firefox Fallback:
const Slider = styled.input`
  @supports (-moz-appearance: none) {
    writing-mode: bt-lr;  /* Firefox vertical slider */
    -moz-appearance: slider-vertical;
  }
  -webkit-appearance: slider-vertical;
`;

Custom Checkbox Support

appearance: none;
Supported in all modern browsers (Chrome, Firefox, Safari, Edge).

Customization Examples

Custom Colors

const SliderWrapper = styled.div`
  background: linear-gradient(to bottom, #f0f0f0, #d0d0d0);
  border: 2px solid #0066cc;
`;

Horizontal Slider

const Slider = styled.input`
  -webkit-appearance: none;
  appearance: none;
  width: 150px;
  height: 10px;
  background: #ddd;
  outline: none;
  
  &::-webkit-slider-thumb {
    -webkit-appearance: none;
    appearance: none;
    width: 20px;
    height: 20px;
    background: #4CAF50;
    cursor: pointer;
    border-radius: 50%;
  }
`;

Icon-Based Mute Button

<button onClick={() => onMuteChange(!isMuted)}>
  {isMuted ? '🔇' : '🔊'}
</button>

Accessibility Improvements

Current implementation lacks accessibility features. Consider adding:
<Slider
  type="range"
  min="0"
  max="100"
  value={isMuted ? 0 : volume}
  onChange={handleSliderChange}
  aria-label="Volume"
  aria-valuenow={isMuted ? 0 : volume}
  aria-valuemin="0"
  aria-valuemax="100"
  role="slider"
/>

<input
  type="checkbox"
  checked={isMuted}
  onChange={handleCheckboxChange}
  aria-label="Mute audio"
  id="mute-checkbox"
/>
<label htmlFor="mute-checkbox">Mute</label>

Testing Considerations

import { render, fireEvent } from '@testing-library/react';
import VolumeSlider from './VolumeSlider';

test('auto-unmutes when slider is dragged', () => {
  const onVolumeChange = jest.fn();
  const onMuteChange = jest.fn();
  
  const { getByRole } = render(
    <VolumeSlider
      volume={50}
      onVolumeChange={onVolumeChange}
      isMuted={true}
      onMuteChange={onMuteChange}
    />
  );
  
  const slider = getByRole('slider');
  fireEvent.change(slider, { target: { value: 75 } });
  
  expect(onVolumeChange).toHaveBeenCalledWith(75);
  expect(onMuteChange).toHaveBeenCalledWith(false);
});

See Also

Build docs developers (and LLMs) love