This Interactive CSS Light Bulb Toggle transforms a standard theme switch into a playful interaction. It creates a convincing illusion of a physical pull-cord using standard click events. Rather than using a complex physics engine, it relies on precise timing between mouse/touch events and CSS transforms to mimic the tension and release of a rope when the user clicks the button.
Core Technique
The component consists of three main layers: the CSS drawing, the variable-based theming, and the event-driven animation.
1. CSS Geometry & Drawing
The light bulb is not an SVG or image; it is constructed entirely from HTML elements styled with CSS borders and transforms. The “glass” bulb uses a border-radius of 100px, while the filament wires are created using rotated divs.
.line-1 {
/* Creating angled filament wires */
width: 10px;
height: 30px;
border-bottom: 0;
transform: rotate(-30deg) translateX(-3px) translateY(50%);
}
2. HSL Variable Theming
The visual change between “Day” and “Night” is handled instantly by toggling a class on the body. The CSS uses HSL variables, allowing for sweeping color changes by modifying just a few lines of code. When the .on class is added, the background becomes dark blue, and the bulb parts adjust contrast accordingly.
body {
/* Day Theme */
--global-bg: hsl(222 4% 96%);
--global-s: hsl(222 99% 46%);
}
body.on {
/* Night Theme */
--global-bg: hsl(228 100% 4%);
--global-s: hsl(254 100% 83%);
}
3. Tactile Feedback via Events
The “pull” effect is a micro-interaction triggered by the button’s press state. The JavaScript separates the mousedown (press) and mouseup (release) events to manipulate the rope element (.lower-2).
- Press: The rope translates down (
translateY(0)), creating the sensation of tension. - Release: The rope snaps back up (
translateY(-4px)), completing the “switch” feeling.
// Simulate tension on press
stateToggle.addEventListener("mousedown", () => {
l2.style.transform = "translateX(13px) translateY(0)";
});
// Snap back on release
stateToggle.addEventListener("mouseup", () => {
l2.style.transform = "translateX(13px) translateY(-4px)";
});
Accessibility (A11y)
- Focusable: The main interaction is wrapped in a
<button>, which is excellent for keyboard navigation. - State Indication: The button lacks an
aria-pressedoraria-labelattribute to indicate the current state (Light/Dark) to screen readers. - Motion: The swinging animation runs infinitely. It should be wrapped in a
@media (prefers-reduced-motion: reduce)query to pause for users sensitive to motion.
Browser Support
The snippet uses standard modern CSS and Vanilla JavaScript. It is compatible with all major browsers.