Menorah Lighting
A Hanukkah menorah. The tall middle candle lights first, then a spark of its flame arcs over to each candle in turn and they light one by one, filling the scene with a warm glow.
How to use it
- Paste the snippet just before
</head>. - Add an empty
<div data-menorah-lighting>where the menorah should stand. The candles are lit when it scrolls into view. It looks best on a dark background.
Options
Add any of these attributes to the same element.
data-loop | Let the candles go out and light them again. |
data-nights | How many candles, 1 to 8. They stand from the right, as on that night of Hanukkah. Default 8. |
data-stagger | Time between one candle and the next. Default 650 ms. |
data-color | Colour of the metal. Default gold #d4a017. |
data-candle | Candle colour. Default alternating blue and white. |
It is 8:5 with a transparent background by default; set your own height or background on the element. With reduced motion every candle is shown lit.
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
(function () {
// Menorah Lighting: a Hanukkah menorah. The tall middle candle (the
// shamash) lights first, then a spark of its flame arcs over to each
// candle in turn and they light one by one, filling the scene with a warm
// glow. Loop it and they go out and are lit again. Drawn as SVG that fills
// the element.
// Usage: <div data-menorah-lighting></div>
// It is 8:5 by default with a transparent background; it looks best on
// something dark.
// Optional: data-loop (go out and light again), data-nights (how many
// candles, 1 to 8, placed from the right as on each night of Hanukkah,
// default 8), data-stagger (ms between candles, default 650),
// data-color (menorah metal, default gold #d4a017), data-candle (candle
// colour, default alternating blue and white).
var DEFAULTS = { nights: 8, stagger: 650, color: '#d4a017' };
var uid = 0;
var CUP = 112, SHAMASH = 86, GAP = 38;
var css =
'[data-menorah-lighting]{display:block;position:relative;overflow:hidden;width:100%;aspect-ratio:8/5}' +
'.mnr-svg{position:absolute;inset:0;width:100%;height:100%;display:block}';
var style = document.createElement('style');
style.textContent = css;
document.head.appendChild(style);
function reduced() { return window.matchMedia('(prefers-reduced-motion: reduce)').matches; }
function flame(g) {
return '<circle r="22" cy="-8" fill="url(#' + g + '-halo)"/>' +
'<path d="M0 0 C-5 -2 -6 -10 0 -20 C6 -10 5 -2 0 0 Z" fill="url(#' + g + '-fire)"/>' +
'<path d="M0 -1 C-2.2 -2.4 -2.2 -6 0 -10 C2.2 -6 2.2 -2.4 0 -1 Z" fill="#fffbeb"/>';
}
function build(el) {
var d = el.dataset;
var n = ++uid;
var g = 'mnr' + n;
var loop = d.loop !== undefined && d.loop !== 'false';
var nights = Math.max(1, Math.min(8, d.nights !== undefined ? Math.round(+d.nights) : DEFAULTS.nights));
var stagger = d.stagger !== undefined ? +d.stagger : DEFAULTS.stagger;
var metal = d.color || DEFAULTS.color;
var candleColor = d.candle;
if (!el.hasAttribute('aria-label')) el.setAttribute('aria-label', 'Menorah candles light one by one');
if (!el.hasAttribute('role')) el.setAttribute('role', 'img');
// Branch positions, left to right; candles stand in the rightmost N.
var xs = [];
for (var i = 4; i >= 1; i--) xs.push(200 - i * GAP);
for (i = 1; i <= 4; i++) xs.push(200 + i * GAP);
var arms = '', cups = '', candles = '';
for (i = 1; i <= 4; i++) {
var r = i * GAP, ry = 18 + i * 16;
arms += '<path d="M' + (200 - r) + ' ' + CUP + ' A' + r + ' ' + ry + ' 0 0 0 ' + (200 + r) + ' ' + CUP + '"/>';
}
var lit = [];
xs.forEach(function (x, k) {
cups += '<path d="M' + (x - 9) + ' ' + (CUP - 3) + ' L' + (x + 9) + ' ' + (CUP - 3) + ' L' + (x + 5) + ' ' + (CUP + 4) + ' L' + (x - 5) + ' ' + (CUP + 4) + ' Z" fill="url(#' + g + '-metal)"/>';
if (k >= 8 - nights) {
var c = candleColor || (k % 2 ? '#f8fafc' : '#93c5fd');
candles += '<rect x="' + (x - 4.5) + '" y="' + (CUP - 37) + '" width="9" height="34" rx="2" fill="' + c + '"/>' +
'<rect x="' + (x - 4.5) + '" y="' + (CUP - 37) + '" width="9" height="34" rx="2" fill="url(#' + g + '-round)"/>' +
'<path d="M' + x + ' ' + (CUP - 37) + ' L' + x + ' ' + (CUP - 41) + '" stroke="#292524" stroke-width="1.5" stroke-linecap="round"/>' +
'<g class="mnr-flame" data-x="' + x + '" data-y="' + (CUP - 40) + '">' + flame(g) + '</g>';
lit.push(x);
}
});
cups += '<path d="M190 ' + (SHAMASH - 3) + ' L210 ' + (SHAMASH - 3) + ' L205 ' + (SHAMASH + 5) + ' L195 ' + (SHAMASH + 5) + ' Z" fill="url(#' + g + '-metal)"/>';
var shamash = '<rect x="195" y="' + (SHAMASH - 41) + '" width="10" height="38" rx="2" fill="' + (candleColor || '#fef3c7') + '"/>' +
'<rect x="195" y="' + (SHAMASH - 41) + '" width="10" height="38" rx="2" fill="url(#' + g + '-round)"/>' +
'<path d="M200 ' + (SHAMASH - 41) + ' L200 ' + (SHAMASH - 45) + '" stroke="#292524" stroke-width="1.5" stroke-linecap="round"/>' +
'<g class="mnr-flame mnr-sham" data-x="200" data-y="' + (SHAMASH - 44) + '">' + flame(g) + '</g>';
el.innerHTML =
'<svg class="mnr-svg" viewBox="0 0 400 250" preserveAspectRatio="xMidYMid meet" aria-hidden="true" focusable="false">' +
'<defs>' +
'<linearGradient id="' + g + '-metal" x1="0" y1="0" x2="1" y2="1"><stop offset="0" stop-color="#fef3c7"/><stop offset=".35" stop-color="' + metal + '"/><stop offset="1" stop-color="#78350f"/></linearGradient>' +
'<linearGradient id="' + g + '-round" x1="0" y1="0" x2="1" y2="0"><stop offset="0" stop-color="#000" stop-opacity=".18"/><stop offset=".4" stop-color="#fff" stop-opacity=".3"/><stop offset="1" stop-color="#000" stop-opacity=".22"/></linearGradient>' +
'<radialGradient id="' + g + '-fire" cx=".5" cy=".8" r=".75"><stop offset="0" stop-color="#fff7d6"/><stop offset=".45" stop-color="#fbbf24"/><stop offset="1" stop-color="#ea580c"/></radialGradient>' +
'<radialGradient id="' + g + '-halo"><stop offset="0" stop-color="#fde68a" stop-opacity=".55"/><stop offset=".5" stop-color="#f97316" stop-opacity=".12"/><stop offset="1" stop-color="#f97316" stop-opacity="0"/></radialGradient>' +
'<radialGradient id="' + g + '-warm" cx=".5" cy=".35"><stop offset="0" stop-color="#fbbf24" stop-opacity=".3"/><stop offset=".45" stop-color="#fbbf24" stop-opacity=".1"/><stop offset="1" stop-color="#fbbf24" stop-opacity="0"/></radialGradient>' +
'</defs>' +
'<ellipse class="mnr-warm" cx="200" cy="90" rx="220" ry="140" fill="url(#' + g + '-warm)" opacity="0"/>' +
'<g fill="none" stroke="url(#' + g + '-metal)" stroke-width="5.5" stroke-linecap="round">' + arms + '</g>' +
'<rect x="196" y="' + SHAMASH + '" width="8" height="128" fill="url(#' + g + '-metal)"/>' +
'<circle cx="200" cy="' + (CUP + 18) + '" r="6" fill="url(#' + g + '-metal)"/>' +
'<path d="M186 214 L214 214 L232 228 L168 228 Z" fill="url(#' + g + '-metal)"/>' +
'<rect x="156" y="228" width="88" height="6" rx="3" fill="url(#' + g + '-metal)"/>' +
cups + candles + shamash +
'<g class="mnr-spark" opacity="0"><circle r="9" fill="url(#' + g + '-halo)"/><circle r="2.6" fill="#fffbeb"/></g>' +
'</svg>';
var svg = el.firstChild;
var warm = svg.querySelector('.mnr-warm');
var sparkEl = svg.querySelector('.mnr-spark');
var F = [];
svg.querySelectorAll('.mnr-flame').forEach(function (f, i) {
F.push({ el: f, x: +f.getAttribute('data-x'), y: +f.getAttribute('data-y'), lit: 0, ph: i * 1.7, f1: 7 + (i * 1.3) % 3, f2: 12 + (i * 2.7) % 5 });
});
var sham = F.pop(); // the shamash is the last flame in the markup
var SP = { u: 0, o: 0, fx: 200, fy: 0, tx: 0, ty: 0 };
var t = 0;
function render(dt) {
t += dt || 0;
var s = t / 1000, total = 0;
F.concat([sham]).forEach(function (f) {
var flick = 0.07 * Math.sin(s * f.f1 + f.ph) + 0.05 * Math.sin(s * f.f2 + f.ph * 2) + 0.03 * Math.sin(s * 29 + f.ph);
var sc = Math.max(0, f.lit);
f.el.setAttribute('transform', 'translate(' + f.x + ' ' + f.y + ') skewX(' + (2.5 * Math.sin(s * 2.2 + f.ph)).toFixed(2) + ') scale(' + (sc * (1 - flick * 0.5)).toFixed(3) + ' ' + (sc * (1 + flick)).toFixed(3) + ')');
total += Math.min(1, sc);
});
warm.setAttribute('opacity', (total / (F.length + 1)).toFixed(3));
if (SP.o > 0) {
var u = SP.u, mx = (SP.fx + SP.tx) / 2, my = Math.min(SP.fy, SP.ty) - 34;
var x = (1 - u) * (1 - u) * SP.fx + 2 * (1 - u) * u * mx + u * u * SP.tx;
var y = (1 - u) * (1 - u) * SP.fy + 2 * (1 - u) * u * my + u * u * SP.ty;
sparkEl.setAttribute('transform', 'translate(' + x.toFixed(1) + ' ' + y.toFixed(1) + ')');
}
sparkEl.setAttribute('opacity', SP.o.toFixed(2));
}
if (reduced()) {
F.forEach(function (f) { f.lit = 1; });
sham.lit = 1;
render(0);
return;
}
render(0);
var tl = null, visible = false, pending = false;
function light() {
if (tl) tl.pause();
tl = anime.createTimeline({
onComplete: function () {
tl = null;
if (!loop) return;
if (visible) light(); else pending = true;
}
});
tl.add(sham, { lit: [0, 1], duration: 700, ease: 'outElastic(1, .55)' }, 300);
var at = 1100;
// Light from left to right, newest candle first.
F.forEach(function (f) {
tl.add(SP, {
u: [0, 1], o: [{ from: 0, to: 1, duration: 80 }, { to: 1, duration: stagger * 0.7 - 160 }, { to: 0, duration: 80 }],
duration: stagger * 0.7, ease: 'inOutSine',
onBegin: function () { SP.fx = sham.x; SP.fy = sham.y - 12; SP.tx = f.x; SP.ty = f.y - 2; }
}, at)
.add(f, { lit: [0, 1], duration: 650, ease: 'outElastic(1, .55)' }, at + stagger * 0.7 - 40);
at += stagger;
});
if (loop) {
at += 2800;
tl.add(F, { lit: 0, duration: 500, ease: 'in(2)', delay: anime.stagger(60) }, at)
.add(sham, { lit: 0, duration: 500, ease: 'in(2)' }, at + F.length * 60 + 200)
.add({ duration: 900 });
}
}
var last = 0;
var timer = anime.createTimer({
loop: true,
duration: 1000,
autoplay: false,
onUpdate: function () {
var now = performance.now();
var dt = last ? Math.min(50, now - last) : 16;
last = now;
render(dt);
}
});
var started = false;
new IntersectionObserver(function (entries) {
visible = entries[0].isIntersecting;
if (visible) { last = 0; timer.play(); if (tl) tl.play(); }
else { timer.pause(); if (tl) tl.pause(); }
if (visible && !started) { started = true; light(); }
else if (visible && pending) { pending = false; light(); }
}, { threshold: 0.25 }).observe(el);
}
function init() {
document.querySelectorAll('[data-menorah-lighting]').forEach(function (el) {
if (el._mnr) return; // idempotent
el._mnr = true;
build(el);
});
}
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
else init();
})();
</script><div data-menorah-lighting style="background: #0f172a"></div>
<!-- the third night, silver, again and again: -->
<div data-menorah-lighting data-loop data-nights="3" data-color="#cbd5e1"></div>