Hier ist etwas Lustiges. Wie könnten wir eine Reihe dieser coolen Matroschka-Puppen erstellen, bei denen sie ineinander verschachtelt sind… aber in CSS?
Diese Idee spielte ich eine Weile in meinem Kopf durch. Dann sah ich einen Tweet von CSS-Tricks und das Artikelbild zeigte die Puppen. Ich nahm das als Zeichen! Es war Zeit, die Finger auf die Tastatur zu legen.

Unser Ziel hier ist es, diese unterhaltsam und interaktiv zu gestalten, sodass wir auf eine Puppe klicken können, um sie zu öffnen und eine weitere, kleinere Puppe freizulegen. Und wir bleiben bei reinem CSS für die Funktionalität. Und während wir schon dabei sind, ersetzen wir die Puppen durch unseren eigenen Charakter, sagen wir einen CodePen-Bären. Etwas in dieser Art

Wir werden uns anfangs nicht darum kümmern, die Dinge schön zu machen. Holen wir uns etwas Markup auf die Seite und klären wir zuerst die Mechanik.
Wir können keine unendliche Anzahl von Puppen haben. Wenn wir die innerste Puppe erreichen, wäre es schön, die Puppen zurückzusetzen, ohne die Seite neu laden zu müssen. Ein netter Trick dafür ist, unsere Szene in ein HTML-Formular zu packen. Auf diese Weise können wir ein Eingabefeld verwenden und das Attribut type auf reset setzen, um jegliches JavaScript zu vermeiden.
<form>
<input type="reset" id="reset"/>
<label for="reset" title="Reset">Reset</label>
</form>
Als Nächstes brauchen wir ein paar Puppen. Oder Bären. Oder etwas zum Anfang. Der Schlüssel hier wird die Verwendung des klassischen Checkbox-Hacks und aller zugehörigen Formular-Labels sein. Als Hinweis: Ich werde Pug verwenden, um das Markup zu handhaben, da es Schleifen unterstützt, was die Dinge etwas einfacher macht. Aber Sie können den HTML-Code sicherlich von Hand schreiben. Hier ist der Anfang mit Formularfeldern und Labels, die den Checkbox-Hack einrichten.
Versuchen Sie, auf einige der Eingaben zu klicken und die Zurücksetzen-Eingabe zu drücken. Alle werden abgewählt. Gut, das werden wir verwenden.
Wir haben etwas Interaktivität, aber noch passiert nichts wirklich. Hier ist der Plan
- Wir zeigen immer nur eine Checkbox an
- Das Aktivieren einer Checkbox sollte das Label für die nächste Checkbox anzeigen.
- Wenn wir zur letzten Checkbox gelangen, sollte unsere einzige Option darin bestehen, das Formular zurückzusetzen.
Der Trick wird darin bestehen, den CSS-Adjacent-Sibling-Combinator (+) zu verwenden.
input:checked + label + input + label {
display: block;
}
Wenn eine Checkbox aktiviert ist, müssen wir das Label für die nächste Puppe anzeigen, die drei Geschwister weiter im DOM ist. Wie machen wir das erste Label sichtbar? Geben Sie ihm über Inline-Stile in unserem Markup ein explizites display: block. Wenn wir das zusammenfügen, erhalten wir etwas in dieser Art
Jedes Label zu klicken, enthüllt das nächste. Halt, das letzte Label wird nicht angezeigt! Das ist richtig. Und das liegt daran, dass das letzte Label keine Checkbox hat. Wir müssen eine Regel hinzufügen, die diesem letzten Label Rechnung trägt.
input:checked + label + input + label,
input:checked + label + label {
display: block;
}
Kühl. Wir kommen voran. Das sind die grundlegenden Mechanismen. Jetzt wird es etwas kniffliger.
Grundlegendes Styling
Sie denken vielleicht: „Warum verstecken wir das aktivierte Label nicht?“ Gute Frage! Aber wenn wir es sofort verstecken, haben wir keinen Übergang zwischen der aktuellen Puppe und der nächsten. Bevor wir mit der Animation unserer Puppen beginnen, erstellen wir grundlegende Boxen, die eine Puppe darstellen. Wir können sie so gestalten, dass sie die Umrisse der Puppe ohne Details nachahmen.
.doll {
color: #fff;
cursor: pointer;
height: 200px;
font-size: 2rem;
left: 50%;
position: absolute;
text-align: center;
top: 50%;
transform: translate(-50%, -50%);
width: 100px;
}
.doll:nth-of-type(even) {
background: #00f;
}
.doll:nth-of-type(odd) {
background: #f00;
}
Wenn man auf eine Puppe klickt, wird die nächste sofort angezeigt, und wenn wir die letzte Puppe erreicht haben, können wir das Formular zurücksetzen, um von vorne zu beginnen. Das ist es, was wir hier wollen.
Die Mechanik
Wir werden die Puppen basierend auf einem Mittelpunkt animieren. Unsere Animation wird aus vielen Schritten bestehen
- Schieben Sie die aktuelle Puppe nach links.
- Öffnen Sie die Puppe, um die nächste freizulegen.
- Verschieben Sie die nächste Puppe dorthin, wo die aktuelle begonnen hat.
- Lassen Sie die aktuelle Puppe ausblenden.
- Weisen Sie die nächste Puppe als aktuelle Puppe zu.
Beginnen wir damit, die aktuelle Puppe nach links zu schieben. Wir wenden eine Animation an, wenn wir ein Label anklicken. Mit dem Pseudo-Selektor :checked können wir die aktuelle Puppe ansprechen. An diesem Punkt ist es erwähnenswert, dass wir CSS-Variablen verwenden werden, um die Animationsgeschwindigkeit und das Verhalten zu steuern. Dies erleichtert das Verketten von Animationen auf den Labels.
:root {
--speed: 0.25;
--base-slide: 100;
--slide-distance: 60;
}
input:checked + label {
animation: slideLeft calc(var(--speed) * 1s) forwards;
}
@keyframes slideLeft {
to {
transform: translate(calc((var(--base-slide) * -1px) + var(--slide-distance) * -1%), 0);
}
}
Das sieht großartig aus. Aber es gibt ein Problem. Sobald wir auf ein Label klicken, könnten wir es erneut anklicken und die Animation zurücksetzen. Das wollen wir nicht.

Wie können wir das umgehen? Wir können Pointer-Events von einem Label entfernen, sobald es angeklickt wurde.
input:checked + label {
animation: slideLeft calc(var(--speed) * 1s) forwards;
pointer-events: none;
}

Großartig! Jetzt, wo wir angefangen haben, können wir die Animationskette nicht mehr stoppen.
Als Nächstes müssen wir die Puppe aufbrechen, um die nächste freizulegen. Hier wird es knifflig, denn wir brauchen zusätzliche Elemente, nicht nur um den Effekt zu erzeugen, dass sich die Puppe öffnet, sondern auch um die nächste Puppe darin freizulegen. Das stimmt: Wir müssen die innere Puppe duplizieren. Der Trick hier ist, eine „falsche“ Puppe freizulegen, die wir gegen die echte austauschen, sobald wir sie animiert haben. Das bedeutet auch, die Anzeige des nächsten Labels zu verzögern.
Nun aktualisiert sich unser Markup, sodass die Labels Span-Elemente enthalten.
<label class="doll" for="doll--1">
<span class="doll doll--dummy"></span>
<span class="doll__half doll__half--top">Top</span>
<span class="doll__half doll__half--bottom">Bottom</span>
</label>
Diese dienen als „Dummy“-Puppe sowie als Deckel und Boden für die aktuelle Puppe.
.doll {
color: #fff;
cursor: pointer;
height: 200px;
font-size: 2rem;
position: absolute;
text-align: center;
width: 100px;
}
.doll:nth-of-type(even) {
--bg: #00f;
--dummy-bg: #f00;
}
.doll:nth-of-type(odd) {
--bg: #f00;
--dummy-bg: #00f;
}
.doll__half {
background: var(--bg);
position: absolute;
width: 100%;
height: 50%;
left: 0;
}
.doll__half--top {
top: 0;
}
.doll__half--bottom {
bottom: 0;
}
.doll__dummy {
background: var(--dummy-bg);
height: 100%;
width: 100%;
position: absolute;
top: 0;
left: 0;
}
Der Deckel erfordert drei Translationen, um den Öffnungseffekt zu erzeugen: eine, um ihn anzuheben, eine, um ihn nach links zu bewegen, und dann eine, um ihn wieder abzusenken.

@keyframes open {
0% {
transform: translate(0, 0);
}
33.333333333333336% {
transform: translate(0, -100%);
}
66.66666666666667% {
transform: translate(-100%, -100%);
}
100% {
transform: translate(-100%, 100%);
}
}
Als Nächstes können wir CSS-Custom-Properties verwenden, um sich ändernde Werte zu handhaben. Sobald die Puppe nach links geschoben wurde, können wir sie öffnen. Aber woher wissen wir, wie lange wir sie verzögern müssen, bis das passiert? Wir können die --speed Custom-Property, die wir zuvor definiert haben, verwenden, um die richtige Verzögerung zu berechnen.
Es sieht ein wenig schnell aus, wenn wir den --speed-Wert so verwenden, wie er ist. Lass ihn uns also mit zwei Sekunden multiplizieren
input:checked + .doll {
animation: slideLeft calc(var(--speed) * 1s) forwards;
pointer-events: none;
}
input:checked + .doll .doll__half--top {
animation: open calc(var(--speed) * 2s) calc(var(--speed) * 1s) forwards; // highlight
}
Viel besser
Jetzt müssen wir die innere „Dummy“-Puppe an die neue Position bewegen. Diese Animation ähnelt der Öffnungsanimation, da sie aus drei Phasen besteht. Wiederum: eine zum Anheben, eine zum Nachrechtsbewegen und eine zum Absetzen. Sie ist auch der Schiebeanimation ähnlich. Wir werden CSS-Custom-Properties verwenden, um die Distanz zu bestimmen, die die Puppe zurücklegt.
:root {
// Introduce a new variable that defines how high the dummy doll should pop out.
--pop-height: 60;
}
@keyframes move {
0% {
transform: translate(0, 0) translate(0, 0);
}
33.333333333333336% {
transform: translate(0, calc(var(--pop-height) * -1%)) translate(0, 0);
}
66.66666666666667% {
transform: translate(0, calc(var(--pop-height) * -1%)) translate(calc((var(--base-slide) * 1px) + var(--slide-distance) * 1%), 0);
}
100% {
transform: translate(0, calc(var(--pop-height) * -1%)) translate(calc((var(--base-slide) * 1px) + var(--slide-distance) * 1%), calc(var(--pop-height) * 1%));
}
}
Fast fertig!
Das Einzige ist, dass die nächste Puppe sofort verfügbar ist, sobald wir auf eine Puppe klicken. Das bedeutet, dass wir uns durch das Set spammen können.

Technisch gesehen sollte die nächste Puppe erst dann erscheinen, wenn die „falsche“ Puppe an ihren Platz bewegt wurde. Erst wenn die „falsche“ Puppe an ihrem Platz ist, können wir sie verstecken und die echte anzeigen. Das bedeutet, wir werden Skalierungsanimationen mit null Sekunden verwenden! Das stimmt. Wir können so tun, als ob wir zwei nullsekündige Animationen verzögern und animation-fill-mode verwenden.
@keyframes appear {
from {
transform: scale(0);
}
}
Wir brauchen eigentlich nur einen Satz @keyframes. Weil wir das, was wir haben, wiederverwenden können, um die umgekehrte Bewegung mit animation-direction: reverse zu erzeugen. In diesem Sinne werden all unsere Animationen ungefähr so angewendet
// The next doll
input:checked + .doll + input + .doll,
// The last doll (doesn't have an input)
input:checked + .doll + .doll {
animation: appear 0s calc(var(--speed) * 5s) both;
display: block;
}
// The current doll
input:checked + .doll,
// The current doll that isn't the first. Specificity prevails
input:checked + .doll + input:checked + .doll {
animation: slideLeft calc(var(--speed) * 1s) forwards;
pointer-events: none;
}
input:checked + .doll .doll__half--top,
input:checked + .doll + input:checked + .doll .doll__half--top {
animation: open calc(var(--speed) * 2s) calc(var(--speed) * 1s) forwards;
}
input:checked + .doll .doll__dummy,
input:checked + .doll + input:checked + .doll .doll__dummy {
animation: move calc(var(--speed) * 2s) calc(var(--speed) * 3s) forwards, appear 0s calc(var(--speed) * 5s) reverse forwards;
}
Beachten Sie, wie wichtig die Variablen sind, insbesondere beim Verkettung von Animationen. Das bringt uns fast dahin, wo wir sein müssen.
Ich höre es schon: „Sie sind alle gleich groß!“ Ja. Das ist das fehlende Stück. Sie müssen kleiner werden. Der Trick hier ist, das Markup erneut anzupassen und wieder CSS-Custom-Properties zu verwenden.
<input id="doll--0" type="checkbox"/>
<label class="doll" for="doll--0" style="display: block; --doll-index: 0;">
<span class="doll__dummy-container">
<span class="doll__dummy"></span>
</span> //highlight
<span class="doll__container">
<span class="doll__half doll__half--top"></span>
<span class="doll__half doll__half--bottom"></span>
</span>
</label>
Wir haben gerade eine CSS-Custom-Property inline eingeführt, die uns den Index der Puppe verrät. Wir können dies verwenden, um eine Skalierung jeder Hälfte sowie der falschen inneren Puppe zu generieren. Die Hälften müssen skaliert werden, um der tatsächlichen Puppengröße zu entsprechen, aber die Skalierung der falschen inneren Puppe muss der der nächsten Puppe entsprechen. Knifflig!
Wir können diese Skalierungen innerhalb der Container anwenden, damit unsere Animationen nicht beeinträchtigt werden.
:root {
--scale-step: 0.05;
}
.doll__container,
.doll__dummy-container {
height: 100%;
left: 0;
position: absolute;
top: 0;
width: 100%;
}
.doll__container {
transform: scale(calc(1 - ((var(--doll-index)) * var(--scale-step))));
transform-origin: bottom;
}
.doll__dummy {
height: 100%;
left: 0;
position: absolute;
top: 0;
transform: scale(calc(1 - ((var(--doll-index) + 1) * var(--scale-step))));
transform-origin: bottom center;
width: 100%;
}
Beachten Sie, wie die Klasse .doll__dummy var(--doll-index) + 1) verwendet, um die Skalierung zu berechnen, damit sie mit der nächsten Puppe übereinstimmt. 👍
Zuletzt weisen wir die Animation der Klasse .doll__dummy-container anstelle der Klasse .doll__dummy neu zu.
input:checked + .doll .doll__dummy-container,
input:checked + .doll + input:checked + .doll .doll__dummy-container {
animation: move calc(var(--speed) * 2s) calc(var(--speed) * 3s) forwards, appear 0s calc(var(--speed) * 5s) reverse forwards;
}
Hier ist eine Demo, bei der den Containern eine Hintergrundfarbe gegeben wurde, um zu sehen, was passiert.
Wir können sehen, dass sich die Inhaltsgröße zwar ändert, sie aber gleich groß bleiben. Dies sorgt für ein konsistentes Animationsverhalten und erleichtert die Wartung des Codes.
Feinschliff
Wow, die Dinge sehen ziemlich schick aus! Wir brauchen nur noch ein paar letzte Handgriffe und wir sind fertig!

Die Szene beginnt unordentlich auszusehen, weil wir die „alten“ Puppen zur Seite schieben, wenn eine neue eingeführt wird. Schieben wir also eine Puppe aus dem Blickfeld, wenn die nächste angezeigt wird, um dieses Durcheinander zu beseitigen.
@keyframes slideOut {
from {
transform: translate(calc((var(--base-slide) * -1px) + var(--slide-distance) * -1%), 0);
}
to {
opacity: 0;
transform: translate(calc((var(--base-slide) * -1px) + var(--slide-distance) * -2%), 0);
}
}
input:checked + .doll,
input:checked + .doll + input:checked + .doll {
animation: slideLeft calc(var(--speed) * 1s) forwards,
slideOut calc(var(--speed) * 1s) calc(var(--speed) * 6s) forwards;
pointer-events: none;
}
Die neue slideOut-Animation blendet die Puppe aus, während sie nach links verschoben wird. Perfekt. 👍

Das ist alles an CSS-Tricks, was wir brauchen, damit dieser Effekt funktioniert. Alles, was übrig bleibt, ist das Styling der Puppen und der Szene.
Wir haben viele Möglichkeiten, die Puppen zu gestalten. Wir könnten ein Hintergrundbild, eine CSS-Illustration, SVG oder was auch immer verwenden. Wir könnten sogar einige Emoji-Puppen zusammenstellen, die zufällige Inline-Farbtöne verwenden!
Wir entscheiden uns für Inline-SVG.
Ich verwende im Grunde die gleichen zugrundeliegenden Mechanismen, die wir bereits behandelt haben. Der Unterschied ist, dass ich auch Inline-Variablen für Farbton und Helligkeit generiere, damit die Bären unterschiedliche Hemdfarben tragen.
Da haben wir es! Stapelnde Puppen – ähm, Bären – mit nichts als HTML und CSS! Der gesamte Code für alle Schritte ist in dieser CodePen-Sammlung verfügbar. Fragen oder Vorschläge? Fühlen Sie sich frei, mich hier in den Kommentaren zu kontaktieren.
Ausgezeichneter Artikel! Das ist eine sehr clevere Idee.
Hallo Jacob!
Danke. Das weiß ich wirklich zu schätzen. Schön, dass es dir gefallen hat!
Ich liebe es! Mehr Dinge sollten nur mit HTML und CSS geschrieben werden, und das ist ein großartiges Beispiel dafür, wie viel man tun kann. <3
Vielen Dank, Tamm! Kommentare wie dieser machen meinen Tag!