Waxing gibbousDay 10 · 76% lit
Nesting Dolls
React + Tailwind
import { useState, type CSSProperties } from 'react';
const DOLLS = 5;
const H0 = 96;
const W0 = 58;
const GAP = 4;
const FLOOR = 124;
const size = (i: number) => ({ w: W0 * 0.8 ** i, h: H0 * 0.8 ** i });
// Lined up largest to smallest; a doll still inside waits behind the last one out.
const lineX = (i: number) => 8 + Array.from({ length: i }, (_, j) => size(j).w + GAP).reduce((a, b) => a + b, 0);
function doll(i: number, n: number) {
const { w, h } = size(i);
const out = i < n;
const host = n - 1;
const x = out ? lineX(i) : lineX(host) + (size(host).w - w) / 2;
return { left: x, top: FLOOR - h, width: w, height: h, zIndex: out ? 10 + i : 5 - i } as CSSProperties;
}
const css = `
.slider-nesting-dolls { --face: light-dark(#fbfcfd, #e5e6e9); --ink: light-dark(#373a40, #121417); display: grid; gap: 12px; width: 230px; }
.slider-nesting-dolls__shelf { position: relative; height: 132px; }
.slider-nesting-dolls__shelf::after { content: ''; position: absolute; left: 0; right: 0; top: ${FLOOR}px; height: 3px; border-radius: 2px; background: light-dark(#dbdde1, #2e3035); }
/* Each doll slides along the shelf; one coming out hops as it goes. */
.slider-nesting-dolls__doll { position: absolute; transition: left 0.5s cubic-bezier(0.3, 1.1, 0.5, 1); }
.slider-nesting-dolls__doll[data-out] .slider-nesting-dolls__hop { animation: slider-nesting-dolls-hop 0.5s ease-out; }
.slider-nesting-dolls__hop { position: absolute; inset: 0; transform-origin: 50% 100%; }
.slider-nesting-dolls__base { position: absolute; left: 0; right: 0; bottom: 0; height: 58%; border-radius: 48% 48% 42% 42% / 34% 34% 66% 66%; background: radial-gradient(ellipse 34% 42% at 50% 46%, color-mix(in oklab, var(--face) 85%, var(--body)) 0 96%, transparent 100%), linear-gradient(90deg, color-mix(in oklab, var(--body) 80%, light-dark(#fbfcfd, #121417)), var(--body) 35%, color-mix(in oklab, var(--body) 75%, light-dark(#373a40, #f1f2f3)) 55%, color-mix(in oklab, var(--body) 82%, light-dark(#fbfcfd, #121417))); }
/* A flower on the apron. */
.slider-nesting-dolls__base::after { content: ''; position: absolute; left: 50%; top: 40%; width: 22%; aspect-ratio: 1; translate: -50% -50%; border-radius: 50%; background: radial-gradient(circle, var(--body) 0 22%, transparent 24%), radial-gradient(circle at 50% 12%, light-dark(#6d7179, #7d8087) 0 18%, transparent 20%), radial-gradient(circle at 88% 50%, light-dark(#6d7179, #7d8087) 0 18%, transparent 20%), radial-gradient(circle at 50% 88%, light-dark(#6d7179, #7d8087) 0 18%, transparent 20%), radial-gradient(circle at 12% 50%, light-dark(#6d7179, #7d8087) 0 18%, transparent 20%); }
.slider-nesting-dolls__top { position: absolute; left: 8%; right: 8%; top: 0; height: 47%; border-radius: 50% 50% 44% 44% / 60% 60% 40% 40%; background: linear-gradient(90deg, color-mix(in oklab, var(--body) 80%, light-dark(#fbfcfd, #121417)), var(--body) 35%, color-mix(in oklab, var(--body) 75%, light-dark(#373a40, #f1f2f3)) 55%, color-mix(in oklab, var(--body) 82%, light-dark(#fbfcfd, #121417))); box-shadow: 0 1.5px 0 color-mix(in oklab, light-dark(#fbfcfd, #121417) 35%, transparent); transform-origin: 30% 100%; }
.slider-nesting-dolls__doll[data-gave] .slider-nesting-dolls__top { animation: slider-nesting-dolls-lift 0.55s ease-in-out; }
.slider-nesting-dolls__face { position: absolute; left: 26%; right: 26%; top: 24%; aspect-ratio: 1; border-radius: 50%; background: radial-gradient(circle at 34% 46%, var(--ink) 0 5%, transparent 7%), radial-gradient(circle at 66% 46%, var(--ink) 0 5%, transparent 7%), radial-gradient(circle at 24% 66%, color-mix(in oklab, light-dark(#6d7179, #7d8087) 45%, transparent) 0 10%, transparent 12%), radial-gradient(circle at 76% 66%, color-mix(in oklab, light-dark(#6d7179, #7d8087) 45%, transparent) 0 10%, transparent 12%), radial-gradient(ellipse 12% 5% at 50% 74%, var(--ink) 0 90%, transparent 100%), var(--face); box-shadow: 0 -2px 0 2px color-mix(in oklab, var(--body) 70%, light-dark(#fbfcfd, #121417)); }
.slider-nesting-dolls__read { display: flex; align-items: baseline; gap: 6px; }
.slider-nesting-dolls__count { color: light-dark(#373a40, #f1f2f3); font-size: 22px; font-weight: 600; font-variant-numeric: tabular-nums; }
.slider-nesting-dolls__unit { color: light-dark(#5f626a, #a7abb2); font-size: 11px; }
.slider-nesting-dolls__track { position: relative; height: 22px; }
.slider-nesting-dolls__rail { position: absolute; left: 11px; right: 11px; top: 9px; height: 4px; border-radius: 2px; background: light-dark(#dbdde1, #2e3035); }
.slider-nesting-dolls__stop { position: absolute; top: 7px; width: 8px; height: 8px; translate: -50% 0; border-radius: 50%; background: light-dark(#cbced3, #393c41); }
.slider-nesting-dolls__thumb { position: absolute; top: 0; left: calc(var(--slider-nesting-dolls-v) * (100% - 22px)); width: 22px; height: 22px; border-radius: 50%; background: light-dark(#373a40, #f1f2f3); box-shadow: 0 2px 6px light-dark(rgba(0, 0, 0, 0.14), rgba(0, 0, 0, 0.45)); transition: left 0.25s cubic-bezier(0.3, 1.3, 0.5, 1); }
.slider-nesting-dolls:has(.slider-nesting-dolls__input:focus-visible) .slider-nesting-dolls__thumb { outline: 2px solid light-dark(#50535b, #c6c8cd); outline-offset: 3px; }
.slider-nesting-dolls__input { position: absolute; inset: 0; width: 100%; height: 100%; margin: 0; opacity: 0; cursor: pointer; -webkit-appearance: none; appearance: none; background: none; }
.slider-nesting-dolls__input::-webkit-slider-thumb { -webkit-appearance: none; width: 22px; height: 22px; }
.slider-nesting-dolls__input::-moz-range-thumb { width: 22px; height: 22px; border: 0; }
@keyframes slider-nesting-dolls-hop { 0% { transform: translateY(0) scale(1, 1); } 30% { transform: translateY(-16px) scale(0.94, 1.06); } 62% { transform: translateY(0) scale(1.08, 0.92); } 80% { transform: translateY(-3px) scale(0.98, 1.02); } 100% { transform: none; } }
@keyframes slider-nesting-dolls-lift { 0% { transform: none; } 35% { transform: translate(-3px, -14px) rotate(-10deg); } 70% { transform: translate(0, -6px) rotate(-3deg); } 100% { transform: none; } }
@media (prefers-reduced-motion: reduce) {
.slider-nesting-dolls * { animation: none !important; transition: none !important; }
}
`;
export function Preview() {
const [n, setN] = useState(3);
return (
<>
<style href="kit-sliders-nesting-dolls" precedence="kit">
{css}
</style>
<div className="slider-nesting-dolls" style={{ '--slider-nesting-dolls-v': (n - 1) / (DOLLS - 1) } as CSSProperties}>
<div className="slider-nesting-dolls__shelf" aria-hidden="true">
{Array.from({ length: DOLLS }, (_, i) => (
<span
key={i}
className="slider-nesting-dolls__doll"
data-out={i > 0 && i < n ? '' : undefined}
data-gave={i + 1 < n ? '' : undefined}
style={{ ...doll(i, n), '--body': i % 2 ? 'light-dark(#6d7179, #a7abb2)' : 'light-dark(#50535b, #7d8087)' } as CSSProperties}
>
<span className="slider-nesting-dolls__hop">
<span className="slider-nesting-dolls__base" />
<span className="slider-nesting-dolls__top">
<span className="slider-nesting-dolls__face" />
</span>
</span>
</span>
))}
</div>
<div className="slider-nesting-dolls__read" aria-hidden="true">
<span className="slider-nesting-dolls__count">{n}</span>
<span className="slider-nesting-dolls__unit">{n === 1 ? 'doll' : 'dolls'}</span>
</div>
<div className="slider-nesting-dolls__track">
<span className="slider-nesting-dolls__rail" aria-hidden="true" />
{Array.from({ length: DOLLS }, (_, i) => (
<span key={i} className="slider-nesting-dolls__stop" style={{ left: `calc(11px + ${i / (DOLLS - 1)} * (100% - 22px))` }} aria-hidden="true" />
))}
<span className="slider-nesting-dolls__thumb" aria-hidden="true" />
<input className="slider-nesting-dolls__input" type="range" min={1} max={DOLLS} value={n} onChange={(e) => setN(+e.target.value)} aria-label="Dolls" aria-valuetext={`${n} ${n === 1 ? 'doll' : 'dolls'}`} />
</div>
</div>
</>
);
}Markup where the component goes, CSS in your stylesheet, script before </body>. In Vue, Svelte or Angular, call initSliderNestingDolls(el) on the mounted element instead.
HTML
<div class="slider-nesting-dolls" style="--slider-nesting-dolls-v:0.5">
<div class="slider-nesting-dolls__shelf" aria-hidden="true">
<span class="slider-nesting-dolls__doll" data-gave="" style="left:8px;top:28px;width:58px;height:96px;z-index:10;--body:light-dark(#50535b, #7d8087)">
<span class="slider-nesting-dolls__hop">
<span class="slider-nesting-dolls__base"></span>
<span class="slider-nesting-dolls__top">
<span class="slider-nesting-dolls__face"></span>
</span>
</span>
</span>
<span class="slider-nesting-dolls__doll" data-out="" data-gave="" style="left:70px;top:47.19999999999999px;width:46.400000000000006px;height:76.80000000000001px;z-index:11;--body:light-dark(#6d7179, #a7abb2)">
<span class="slider-nesting-dolls__hop">
<span class="slider-nesting-dolls__base"></span>
<span class="slider-nesting-dolls__top">
<span class="slider-nesting-dolls__face"></span>
</span>
</span>
</span>
<span class="slider-nesting-dolls__doll" data-out="" style="left:120.4px;top:62.55999999999999px;width:37.120000000000005px;height:61.44000000000001px;z-index:12;--body:light-dark(#50535b, #7d8087)">
<span class="slider-nesting-dolls__hop">
<span class="slider-nesting-dolls__base"></span>
<span class="slider-nesting-dolls__top">
<span class="slider-nesting-dolls__face"></span>
</span>
</span>
</span>
<span class="slider-nesting-dolls__doll" style="left:124.11200000000001px;top:74.84799999999998px;width:29.69600000000001px;height:49.152000000000015px;z-index:2;--body:light-dark(#6d7179, #a7abb2)">
<span class="slider-nesting-dolls__hop">
<span class="slider-nesting-dolls__base"></span>
<span class="slider-nesting-dolls__top">
<span class="slider-nesting-dolls__face"></span>
</span>
</span>
</span>
<span class="slider-nesting-dolls__doll" style="left:127.08160000000001px;top:84.6784px;width:23.756800000000005px;height:39.321600000000004px;z-index:1;--body:light-dark(#50535b, #7d8087)">
<span class="slider-nesting-dolls__hop">
<span class="slider-nesting-dolls__base"></span>
<span class="slider-nesting-dolls__top">
<span class="slider-nesting-dolls__face"></span>
</span>
</span>
</span>
</div>
<div class="slider-nesting-dolls__read" aria-hidden="true">
<span class="slider-nesting-dolls__count">3</span>
<span class="slider-nesting-dolls__unit">dolls</span>
</div>
<div class="slider-nesting-dolls__track">
<span class="slider-nesting-dolls__rail" aria-hidden="true"></span>
<span class="slider-nesting-dolls__stop" style="left:calc(11px + 0 * (100% - 22px))" aria-hidden="true"></span>
<span class="slider-nesting-dolls__stop" style="left:calc(11px + 0.25 * (100% - 22px))" aria-hidden="true"></span>
<span class="slider-nesting-dolls__stop" style="left:calc(11px + 0.5 * (100% - 22px))" aria-hidden="true"></span>
<span class="slider-nesting-dolls__stop" style="left:calc(11px + 0.75 * (100% - 22px))" aria-hidden="true"></span>
<span class="slider-nesting-dolls__stop" style="left:calc(11px + 1 * (100% - 22px))" aria-hidden="true"></span>
<span class="slider-nesting-dolls__thumb" aria-hidden="true"></span>
<input class="slider-nesting-dolls__input" type="range" min="1" max="5" aria-label="Dolls" aria-valuetext="3 dolls" value="3"/>
</div>
</div>CSS
.slider-nesting-dolls { --face: light-dark(#fbfcfd, #e5e6e9); --ink: light-dark(#373a40, #121417); display: grid; gap: 12px; width: 230px; }
.slider-nesting-dolls__shelf { position: relative; height: 132px; }
.slider-nesting-dolls__shelf::after { content: ''; position: absolute; left: 0; right: 0; top: 124px; height: 3px; border-radius: 2px; background: light-dark(#dbdde1, #2e3035); }
/* Each doll slides along the shelf; one coming out hops as it goes. */
.slider-nesting-dolls__doll { position: absolute; transition: left 0.5s cubic-bezier(0.3, 1.1, 0.5, 1); }
.slider-nesting-dolls__doll[data-out] .slider-nesting-dolls__hop { animation: slider-nesting-dolls-hop 0.5s ease-out; }
.slider-nesting-dolls__hop { position: absolute; inset: 0; transform-origin: 50% 100%; }
.slider-nesting-dolls__base { position: absolute; left: 0; right: 0; bottom: 0; height: 58%; border-radius: 48% 48% 42% 42% / 34% 34% 66% 66%; background: radial-gradient(ellipse 34% 42% at 50% 46%, color-mix(in oklab, var(--face) 85%, var(--body)) 0 96%, transparent 100%), linear-gradient(90deg, color-mix(in oklab, var(--body) 80%, light-dark(#fbfcfd, #121417)), var(--body) 35%, color-mix(in oklab, var(--body) 75%, light-dark(#373a40, #f1f2f3)) 55%, color-mix(in oklab, var(--body) 82%, light-dark(#fbfcfd, #121417))); }
/* A flower on the apron. */
.slider-nesting-dolls__base::after { content: ''; position: absolute; left: 50%; top: 40%; width: 22%; aspect-ratio: 1; translate: -50% -50%; border-radius: 50%; background: radial-gradient(circle, var(--body) 0 22%, transparent 24%), radial-gradient(circle at 50% 12%, light-dark(#6d7179, #7d8087) 0 18%, transparent 20%), radial-gradient(circle at 88% 50%, light-dark(#6d7179, #7d8087) 0 18%, transparent 20%), radial-gradient(circle at 50% 88%, light-dark(#6d7179, #7d8087) 0 18%, transparent 20%), radial-gradient(circle at 12% 50%, light-dark(#6d7179, #7d8087) 0 18%, transparent 20%); }
.slider-nesting-dolls__top { position: absolute; left: 8%; right: 8%; top: 0; height: 47%; border-radius: 50% 50% 44% 44% / 60% 60% 40% 40%; background: linear-gradient(90deg, color-mix(in oklab, var(--body) 80%, light-dark(#fbfcfd, #121417)), var(--body) 35%, color-mix(in oklab, var(--body) 75%, light-dark(#373a40, #f1f2f3)) 55%, color-mix(in oklab, var(--body) 82%, light-dark(#fbfcfd, #121417))); box-shadow: 0 1.5px 0 color-mix(in oklab, light-dark(#fbfcfd, #121417) 35%, transparent); transform-origin: 30% 100%; }
.slider-nesting-dolls__doll[data-gave] .slider-nesting-dolls__top { animation: slider-nesting-dolls-lift 0.55s ease-in-out; }
.slider-nesting-dolls__face { position: absolute; left: 26%; right: 26%; top: 24%; aspect-ratio: 1; border-radius: 50%; background: radial-gradient(circle at 34% 46%, var(--ink) 0 5%, transparent 7%), radial-gradient(circle at 66% 46%, var(--ink) 0 5%, transparent 7%), radial-gradient(circle at 24% 66%, color-mix(in oklab, light-dark(#6d7179, #7d8087) 45%, transparent) 0 10%, transparent 12%), radial-gradient(circle at 76% 66%, color-mix(in oklab, light-dark(#6d7179, #7d8087) 45%, transparent) 0 10%, transparent 12%), radial-gradient(ellipse 12% 5% at 50% 74%, var(--ink) 0 90%, transparent 100%), var(--face); box-shadow: 0 -2px 0 2px color-mix(in oklab, var(--body) 70%, light-dark(#fbfcfd, #121417)); }
.slider-nesting-dolls__read { display: flex; align-items: baseline; gap: 6px; }
.slider-nesting-dolls__count { color: light-dark(#373a40, #f1f2f3); font-size: 22px; font-weight: 600; font-variant-numeric: tabular-nums; }
.slider-nesting-dolls__unit { color: light-dark(#5f626a, #a7abb2); font-size: 11px; }
.slider-nesting-dolls__track { position: relative; height: 22px; }
.slider-nesting-dolls__rail { position: absolute; left: 11px; right: 11px; top: 9px; height: 4px; border-radius: 2px; background: light-dark(#dbdde1, #2e3035); }
.slider-nesting-dolls__stop { position: absolute; top: 7px; width: 8px; height: 8px; translate: -50% 0; border-radius: 50%; background: light-dark(#cbced3, #393c41); }
.slider-nesting-dolls__thumb { position: absolute; top: 0; left: calc(var(--slider-nesting-dolls-v) * (100% - 22px)); width: 22px; height: 22px; border-radius: 50%; background: light-dark(#373a40, #f1f2f3); box-shadow: 0 2px 6px light-dark(rgba(0, 0, 0, 0.14), rgba(0, 0, 0, 0.45)); transition: left 0.25s cubic-bezier(0.3, 1.3, 0.5, 1); }
.slider-nesting-dolls:has(.slider-nesting-dolls__input:focus-visible) .slider-nesting-dolls__thumb { outline: 2px solid light-dark(#50535b, #c6c8cd); outline-offset: 3px; }
.slider-nesting-dolls__input { position: absolute; inset: 0; width: 100%; height: 100%; margin: 0; opacity: 0; cursor: pointer; -webkit-appearance: none; appearance: none; background: none; }
.slider-nesting-dolls__input::-webkit-slider-thumb { -webkit-appearance: none; width: 22px; height: 22px; }
.slider-nesting-dolls__input::-moz-range-thumb { width: 22px; height: 22px; border: 0; }
@keyframes slider-nesting-dolls-hop { 0% { transform: translateY(0) scale(1, 1); } 30% { transform: translateY(-16px) scale(0.94, 1.06); } 62% { transform: translateY(0) scale(1.08, 0.92); } 80% { transform: translateY(-3px) scale(0.98, 1.02); } 100% { transform: none; } }
@keyframes slider-nesting-dolls-lift { 0% { transform: none; } 35% { transform: translate(-3px, -14px) rotate(-10deg); } 70% { transform: translate(0, -6px) rotate(-3deg); } 100% { transform: none; } }
@media (prefers-reduced-motion: reduce) {
.slider-nesting-dolls * { animation: none !important; transition: none !important; }
}JavaScript
function initSliderNestingDolls(root) {
var input = root.querySelector('.slider-nesting-dolls__input');
var dolls = root.querySelectorAll('.slider-nesting-dolls__doll');
var count = root.querySelector('.slider-nesting-dolls__count');
var unit = root.querySelector('.slider-nesting-dolls__unit');
var floor = 124;
var gap = 4;
function size(i) { return { w: 58 * Math.pow(0.8, i), h: 96 * Math.pow(0.8, i) }; }
function lineX(i) {
var x = 8;
for (var j = 0; j < i; j++) x += size(j).w + gap;
return x;
}
function update() {
var n = +input.value;
var host = n - 1;
dolls.forEach(function (doll, i) {
var s = size(i);
var out = i < n;
// Lined up largest to smallest; a doll still inside waits behind the last one out.
var x = out ? lineX(i) : lineX(host) + (size(host).w - s.w) / 2;
doll.style.left = x + 'px';
doll.style.top = floor - s.h + 'px';
doll.style.zIndex = out ? 10 + i : 5 - i;
// Coming out it hops; the one it came from lifts its top off.
doll.toggleAttribute('data-out', i > 0 && out);
doll.toggleAttribute('data-gave', i + 1 < n);
});
root.style.setProperty('--slider-nesting-dolls-v', (n - 1) / (dolls.length - 1));
count.textContent = n;
unit.textContent = n === 1 ? 'doll' : 'dolls';
input.setAttribute('aria-valuetext', n + (n === 1 ? ' doll' : ' dolls'));
}
input.addEventListener('input', update);
update();
return function () { input.removeEventListener('input', update); };
}
document.querySelectorAll('.slider-nesting-dolls').forEach(initSliderNestingDolls);Goal: Create a "Nesting Dolls" slider.
Visual behavior: Choose how many dolls: each new one hops out of the last as its top lifts off, and they line up by size.
Technique:
- One real <input type="range"> lies transparent over the track (its thumb sized to the drawn handle, so the pointer lines up), and everything drawn is set from its value.
- The dolls line up largest to smallest. A doll still inside waits centred behind the last one out, and slides along when it comes out.
- Coming out it hops (a squash and stretch keyframe that plays when it gains data-out), and the doll it came from lifts its top off for a moment.
- Each doll is a body, a top and a face drawn with gradients, with a flower on the apron.
Accessibility: A native range input named "Dolls" with an aria-valuetext in words, so a screen reader announces the reading rather than a bare number; keyboard focus outlines the drawn handle. The drawing is aria-hidden. Under prefers-reduced-motion the dolls move without hops.
My stack: {{USER_STACK}}
My styling: {{USER_STYLING}}
My constraints: {{USER_CONSTRAINTS}}
My colors: neutral only. Black, white and grays, no hues. Use the color values above as written, or my project's closest neutral tokens.
Return a single self-contained component. No dependencies.Goal: Create a "Nesting Dolls" slider.
Visual behavior: Choose how many dolls: each new one hops out of the last as its top lifts off, and they line up by size.
Technique:
- One real <input type="range"> lies transparent over the track (its thumb sized to the drawn handle, so the pointer lines up), and everything drawn is set from its value.
- The dolls line up largest to smallest. A doll still inside waits centred behind the last one out, and slides along when it comes out.
- Coming out it hops (a squash and stretch keyframe that plays when it gains data-out), and the doll it came from lifts its top off for a moment.
- Each doll is a body, a top and a face drawn with gradients, with a flower on the apron.
Accessibility: A native range input named "Dolls" with an aria-valuetext in words, so a screen reader announces the reading rather than a bare number; keyboard focus outlines the drawn handle. The drawing is aria-hidden. Under prefers-reduced-motion the dolls move without hops.
My stack: {{USER_STACK}}
My styling: {{USER_STYLING}}
My constraints: {{USER_CONSTRAINTS}}
My colors: use my project's existing color tokens. The color values above are grayscale placeholders. Map each one by its role (surface, text, border, accent, glow) to my theme, and keep their relative lightness so contrast and the effect still read.
Return a single self-contained component. No dependencies.Paste into Claude, ChatGPT or Cursor after filling in {{USER_STACK}}, {{USER_STYLING}} and {{USER_CONSTRAINTS}}. Neutral keeps the greys you see here; Match my project asks the model to map them onto your own colour tokens.
Free to use in any project, commercial included. MIT-0, no attribution required. Licence
About this component
Choose how many dolls: each new one hops out of the last as its top lifts off, and they line up by size.
- Dependencies
- None
- Difficulty
- Intermediate
- Triggers
- state change, spring, keyframe
- Techniques
- css transition, keyframes, radial gradient
- Reduced motion
- Respected
- Theme
- Light and dark (follows your page's color-scheme)
A native range input named "Dolls" with an aria-valuetext in words, so a screen reader announces the reading rather than a bare number; keyboard focus outlines the drawn handle. The drawing is aria-hidden. Under prefers-reduced-motion the dolls move without hops.