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
Current volume level (0-100)
Callback when volume slider changes(newVolume: number) => void
Callback when mute checkbox is toggled
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:
- User mutes audio
- User drags volume slider
- 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
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%;
}
`;
<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