A Christmas-lights alphabet wall as a drop-in CSS + JS plugin. Type a message, the wall spells it out one bulb at a time — inspired by Joyce Byers' wall in Stranger Things S1. Ships with built-in Christmas and Fourth of July themes as counter-examples so the API doesn't end up shrink-wrapped to one aesthetic.
goboldlyforward.github.io/joycelights — toggle the Stranger Things / Christmas / Fourth of July tabs to see the same plugin themed three different ways.
Renders a hand-painted alphabet wall (wallpaper backdrop, paint-drip letters, multicolor Christmas bulbs on a sagging wire) into any container, then animates messages onto it. Each letter's bulb flashes in sequence then goes dark before the next one fires.
For now, download joycelights.css and joycelights.js from this repo. (npm publication pending.)
<link rel="stylesheet" href="path/to/joycelights.css">
<script src="path/to/joycelights.js"></script><div id="wall"></div>
<script>
const wall = new JoyceLights('#wall');
wall.spell('RIGHT HERE');
</script>new JoyceLights(target, options?) mounts the wall into target (selector or element). spell(message) returns a Promise that resolves when the message has finished playing.
// Built-in: Stranger Things (default)
new JoyceLights('#wall');
// Built-in: Christmas
wall.setOptions({
theme: 'christmas',
palette: ['red', 'green'],
drips: false,
letterTilt: 0, letterShift: 0, letterScale: false,
});
// Built-in: Fourth of July
wall.setOptions({
theme: 'july4',
palette: ['red', 'white', 'blue'],
drips: false,
randomColors: true,
letterTilt: 0, letterShift: 0, letterScale: false,
});Roll your own theme by adding a modifier class (.joycelights--mygame) and overriding the CSS custom properties — see Theming.
red · green · blue · yellow · orange · white
Pass any subset (or all) via palette to control which colors letters cycle through. Combine with colorMap to pin specific letters to specific colors, or randomColors: true to re-roll on each spell.
To make the wallpaper extend behind an entire region (header, hero), put the theme modifier on a wrapper and pass flat: true to the wall so it drops its own chrome:
<header class="stage joycelights--july4">
<h1>Independence Day</h1>
<div id="wall"></div>
</header>
<style>
.stage {
background-color: var(--joycelights-wallpaper-base);
background-image: var(--joycelights-wallpaper-image);
background-repeat: repeat;
background-size: 160px 160px;
}
</style>
<script>
new JoyceLights('#wall', { flat: true });
</script>CSS custom properties cascade from the wrapper into the wall, so one class swap reskins everything.
new JoyceLights('#wall', {
rows: ['ABCDEFGH', 'IJKLMNOPQ', 'RSTUVWXYZ'],
palette: ['red', 'green', 'blue', 'yellow', 'orange'],
colorMap: { H: 'red', E: 'red', L: 'red', O: 'red' },
randomColors: false,
flicker: true,
speed: 600, // ms per letter cycle
onTimeRatio: 0.7,
drips: true,
wallpaper: true,
flat: false, // drop ALL wall chrome (parent provides bg)
theme: null, // 'christmas' | 'july4' | null
letterTilt: 14, // ± degrees of random rotation per letter
letterShift: 6, // ± pixels of random vertical shift
letterScale: true, // random 0.9–1.12 scale per letter
wireSag: 10, // 0–14 viewBox units; depth of wire dip between bulbs
onLetter: (letter, color, index) => { /* sfx, etc. */ },
});wall.spell('HELLO');
wall.spell('RUN', { speed: 900 });
wall.light('A', 'red');
wall.unlight('A');
wall.clear();
wall.cancel();
wall.setOptions({ theme: 'july4' });
wall.destroy();| Class | What it does |
|---|---|
.joycelights--christmas |
Built-in festive theme (cream + holly + red letters) |
.joycelights--july4 |
Built-in Americana theme (cream + stars + navy letters) |
.joycelights--no-wallpaper |
Drops wallpaper bg + shadow (keeps padding + radius) |
.joycelights--flat |
Drops all chrome — use when a parent provides the bg |
.joycelights--no-drips |
Hides letter paint drips |
.joycelights {
--joycelights-paint: #1a1612;
--joycelights-wallpaper-base: #dec59b;
--joycelights-wallpaper-image: url("…SVG data URI…");
--joycelights-wire: #1a2a14;
--joycelights-letter-font: 'Permanent Marker', cursive;
--joycelights-letter-shadow: 0 1px 0 rgba(0,0,0,0.5), 1px 0 0 rgba(0,0,0,0.3);
--joycelights-vignette: radial-gradient(/* … */);
--joycelights-shadow: inset 0 0 60px rgba(60,30,10,0.35), 0 6px 30px rgba(0,0,0,0.6);
--joycelights-radius: 4px;
}
/* Your own theme */
.joycelights--retro {
--joycelights-paint: #ff00ff;
--joycelights-wallpaper-base: #150030;
--joycelights-letter-font: 'Press Start 2P', monospace;
}Activate with wall.setOptions({ theme: 'retro' }).
Each row's wire is an inline SVG with a single <path> of stitched-together quadratic Bezier curves. The path anchors at every bulb's x-position and dips down to wireSag viewBox units between each pair. preserveAspectRatio="none" lets the SVG stretch responsively across whatever pixel width the row ends up at, and vector-effect="non-scaling-stroke" keeps the stroke a consistent pixel thickness. The wire SVG sits at z-index 1, below the bulbs' z-index 3 screw caps — so the wire visually threads through each cap.
HTML, CSS, and a smidge of JavaScript. No framework, no build step.
- Working prototype
- LICENSE (MIT)
-
.gitignore - README
-
package.json - CSS + JS split, public API
- Themeable (CSS vars +
themeoption) - Built-in Christmas theme
- Built-in Fourth of July theme
- White bulb in core palette
- Curved/sagging wire (SVG path per row)
- Full-bleed-friendly via
flatoption - Publish to npm (as
@goboldlyforward/joycelights) - Optional
joycelights-railsgem wrapper - GitHub Actions CI (stylelint + eslint)
- Deploy demo to gh-pages
- Multi-strand chaotic wire (multiple overlapping paths)
- Click-to-light interaction on individual letters
- Ambient
twinkle()mode (multiple bulbs gently fading independently) - Optional bulb "ping" SFX hook
MIT — see LICENSE.