Magnetic button
CSS + JSInside its field the button shifts and leans toward the pointer while its label floats higher, so the two slide apart. JS only reports the pointer position; release it and a springy transition takes it home.
Five thin star plates, each a front and a gold back with an edge between them. Radio inputs and labels choose a rating, and every star up to it flips over, one after another.
Your edited version
--u, tied to the canvas — the stars, the thickness between their layers, the caption — so the row is the same share of a gallery card, the editor and a recording canvas. The numbers below are those units.clip-path star stacked 1 unit apart: the front (plain) and back (gold) faces with three edge layers between them, so a turning star shows a real thickness.rotateY(180deg) translateZ(2 units): it faces the other way, and once the star turns half round it reads correctly. backface-visibility: hidden on both faces means only the one facing you is drawn.~ can reach everything after them. Rating r is one rule: #star-r:checked ~ .stars label:nth-child(-n + r) selects the first r stars, which turn over.transition-delay: calc(var(--i) * 90ms) lights them left to right; the resting style has the reverse delay, so a lower rating empties the row from the right. The delay in the state you go to is the one that counts.#star-n:checked ~ label:nth-child(-n + n)clip-path star faces, stacked edge layersrotateY(180deg) + backface-visibilitytransition-delay from --i<div class="rating" role="radiogroup" aria-label="Rate this effect">
<b class="title" aria-hidden="true">Rate this effect</b>
<input type="radio" name="rating" id="star-1" aria-label="1 star, Poor" />
<input type="radio" name="rating" id="star-2" aria-label="2 stars, Okay" />
<input type="radio" name="rating" id="star-3" aria-label="3 stars, Good" />
<input type="radio" name="rating" id="star-4" aria-label="4 stars, Great" checked />
<input type="radio" name="rating" id="star-5" aria-label="5 stars, Amazing!" />
<div class="stars">
<label for="star-1" style="--i:0"><span class="lift"><span class="star"><i></i><i></i><i></i><i></i><i></i></span></span></label>
<label for="star-2" style="--i:1"><span class="lift"><span class="star"><i></i><i></i><i></i><i></i><i></i></span></span></label>
<label for="star-3" style="--i:2"><span class="lift"><span class="star"><i></i><i></i><i></i><i></i><i></i></span></span></label>
<label for="star-4" style="--i:3"><span class="lift"><span class="star"><i></i><i></i><i></i><i></i><i></i></span></span></label>
<label for="star-5" style="--i:4"><span class="lift"><span class="star"><i></i><i></i><i></i><i></i><i></i></span></span></label>
</div>
<p class="words" aria-hidden="true"><span>Tap a star</span><span>Poor</span><span>Okay</span><span>Good</span><span>Great</span><span>Amazing!</span></p>
</div>
.rating {
/* one base unit: every length below is a multiple of it, so the row is the same share of a
card, the editor, a full screen and a recording canvas */
--u: 0.38vmin;
display: grid;
justify-items: center;
/* five stars in a row is a wide shape. The caption and the gaps carry the height: at the old
11/12/14 they did not, and the model came out at the 92vmin width limit and under the
40vmin floor */
gap: calc(18 * var(--u));
perspective: calc(800 * var(--u));
transform-style: preserve-3d;
}
/* the real radios stay (arrow keys, forms, screen readers), only invisible */
.rating input {
position: absolute;
width: 1px;
height: 1px;
margin: 0;
opacity: 0;
pointer-events: none;
}
.title {
/* the stage's own ink, softened with opacity: a fixed pale lavender was 2.5:1 on the light stage */
color: inherit;
opacity: 0.72;
font-size: calc(16 * var(--u));
font-weight: 800;
letter-spacing: 0.14em;
text-transform: uppercase;
}
.stars {
display: flex;
gap: calc(9 * var(--u));
transform-style: preserve-3d;
}
/* the label is the static hit target; only the star inside it moves */
.stars label {
position: relative;
width: calc(34 * var(--u));
height: calc(34 * var(--u));
border-radius: calc(8 * var(--u));
cursor: pointer;
transform-style: preserve-3d;
}
/* a ring that bursts once as the star lands gold */
.stars label::before {
content: '';
position: absolute;
inset: calc(-5 * var(--u));
border: calc(2 * var(--u)) solid #ffb547;
border-radius: 50%;
opacity: 0;
pointer-events: none;
}
/* a warm glow behind a gold star */
.stars label::after {
content: '';
position: absolute;
inset: calc(-10 * var(--u));
border-radius: 50%;
background: radial-gradient(closest-side, rgb(255 181 71 / 0.45), transparent);
opacity: 0;
pointer-events: none;
transform: translateZ(calc(-6 * var(--u)));
transition: opacity 0.3s;
}
.lift {
position: absolute;
inset: 0;
pointer-events: none;
transform-style: preserve-3d;
transition: transform 0.25s cubic-bezier(0.3, 1.6, 0.5, 1);
}
label:hover .lift {
transform: translateZ(calc(12 * var(--u))) scale(1.14);
}
.star {
position: absolute;
inset: 0;
transform-style: preserve-3d;
transition: transform 0.7s cubic-bezier(0.35, 1.45, 0.5, 1);
transition-delay: calc((4 - var(--i)) * 60ms); /* turning back: the last star first */
}
/* five copies of one star shape, 1 unit apart */
.star i {
position: absolute;
inset: 0;
clip-path: polygon(50% 2%, 61.5% 36%, 97% 36.5%, 68.5% 57.5%, 79% 92%, 50% 71%, 21% 92%, 31.5% 57.5%, 3% 36.5%, 38.5% 36%);
background: color-mix(in srgb, #ffb547 40%, #1b1408); /* the edge */
}
.star i:nth-child(2) { transform: translateZ(calc(1 * var(--u))); }
.star i:nth-child(4) { transform: translateZ(calc(-1 * var(--u))); }
/* front: a darker star inside a lighter one, so it looks outlined */
.star i:nth-child(1) {
background: color-mix(in srgb, #eceefb 38%, #141830);
backface-visibility: hidden;
transform: translateZ(calc(2 * var(--u)));
}
.star i:nth-child(1)::before {
content: '';
position: absolute;
inset: calc(3 * var(--u));
clip-path: inherit;
background: color-mix(in srgb, #eceefb 12%, #141830);
}
/* back: gold, facing the other way */
.star i:nth-child(5) {
background: linear-gradient(160deg, #ffe2b8 10%, #ffb547 45%, #e58a2e 90%);
backface-visibility: hidden;
transform: rotateY(180deg) translateZ(calc(2 * var(--u)));
}
/* rating r turns stars 1 to r, one after another */
#star-1:checked ~ .stars label:nth-child(-n + 1) .star,
#star-2:checked ~ .stars label:nth-child(-n + 2) .star,
#star-3:checked ~ .stars label:nth-child(-n + 3) .star,
#star-4:checked ~ .stars label:nth-child(-n + 4) .star,
#star-5:checked ~ .stars label:nth-child(-n + 5) .star {
transform: rotateY(180deg);
transition-delay: calc(var(--i) * 90ms);
}
#star-1:checked ~ .stars label:nth-child(-n + 1)::after,
#star-2:checked ~ .stars label:nth-child(-n + 2)::after,
#star-3:checked ~ .stars label:nth-child(-n + 3)::after,
#star-4:checked ~ .stars label:nth-child(-n + 4)::after,
#star-5:checked ~ .stars label:nth-child(-n + 5)::after {
opacity: 1;
transition-delay: calc(var(--i) * 90ms + 200ms);
}
/* a newly matching rule starts its animation: the ring bursts once per star lit */
#star-1:checked ~ .stars label:nth-child(-n + 1)::before,
#star-2:checked ~ .stars label:nth-child(-n + 2)::before,
#star-3:checked ~ .stars label:nth-child(-n + 3)::before,
#star-4:checked ~ .stars label:nth-child(-n + 4)::before,
#star-5:checked ~ .stars label:nth-child(-n + 5)::before {
animation: burst 0.65s calc(var(--i) * 90ms + 250ms) ease-out both;
}
@keyframes burst {
from { opacity: 0.9; transform: translateZ(calc(-4 * var(--u))) scale(0.5); }
to { opacity: 0; transform: translateZ(calc(-4 * var(--u))) scale(1.5); }
}
/* keyboard focus: ring the star whose radio has focus */
#star-1:focus-visible ~ .stars label:nth-child(1),
#star-2:focus-visible ~ .stars label:nth-child(2),
#star-3:focus-visible ~ .stars label:nth-child(3),
#star-4:focus-visible ~ .stars label:nth-child(4),
#star-5:focus-visible ~ .stars label:nth-child(5) {
outline: calc(2 * var(--u)) solid #2ee6d6;
outline-offset: calc(5 * var(--u));
}
/* the caption: all words in one grid cell, only the chosen one shows */
.words {
display: grid;
margin: 0;
/* a deep gold, 3:1 or more on both stages (this is large text): the stars' bright #ffb547 was
1.6:1 on the light one */
color: #c47a00;
font-size: calc(26 * var(--u));
font-weight: 800;
}
.words span {
grid-area: 1 / 1;
justify-self: center;
opacity: 0;
transform: translateY(calc(5 * var(--u)));
transition: opacity 0.25s, transform 0.25s;
}
.words span:first-child {
color: inherit; /* the stage's own ink: a fixed pale lavender vanished on the light stage */
opacity: 1;
transform: none;
}
.rating input:checked ~ .words span:first-child {
opacity: 0;
transform: translateY(calc(-5 * var(--u)));
}
#star-1:checked ~ .words span:nth-child(2),
#star-2:checked ~ .words span:nth-child(3),
#star-3:checked ~ .words span:nth-child(4),
#star-4:checked ~ .words span:nth-child(5),
#star-5:checked ~ .words span:nth-child(6) {
opacity: 1;
transform: none;
}