Save button
A save that is confirmed twice. Click the bookmark and it fills while one swell runs down it, top to tip, as if the ribbon lifted toward you. Beside it, the Saved row that keeps the count answers: its icon pops, the digit that changed rolls over, a ring leaves the row, and a moment later the same swell runs down its bookmark. Taking the save back is quiet. The icon is the plain outline at rest and is only redrawn while the swell runs. The wave, the pop, the ring, and the gap between the two are all controls.
Build prompt
InteractionBuild a save interaction made of two parts that sit side by side with a clear gap between them: on the left a "Saved" row (bookmark icon, the word Saved, and a count that starts at 12), on the right a 32px square icon button holding a bookmark. The row is a display, not a control: 172 x 36, 8px radius, a raised surface with a 1px hairline ring, icon 16px, label 12.5px medium, count 11px muted with tabular numerals. The button has a hairline border and a muted icon that turns to the ink colour on hover and when saved; it carries aria-pressed and its label switches between "Save" and "Remove from saved". Grayscale, theme tokens only. Clicking the button saves: its bookmark fills at once and one swell runs down it. In the same click the row answers: the count becomes 13, with only the digit that changed rolling over (see rule 9), the row's icon pops, a ring leaves the row, and 27% of the way into the pop (about 170ms after the click) the same swell runs down the row's bookmark, overlapping the pop. Clicking again unsaves: the bookmark empties and the count drops back to 12, the changed digit rolling the other way. No swell, no pop, no ring on an unsave. Rules that make it feel right, do not skip any: 1. At rest the bookmark is the ordinary icon path (Lucide bookmark, 24 grid, round caps and joins). Only while the swell runs is the path replaced, by the same outline rebuilt as a dense polyline (a point every half unit up both sides, along the top edge and the notch, eight steps round each top corner, about 130 points). When the swell ends, put the original path back. Set overflow visible on the svg: the swell is wider than the icon's box. 2. The swell is a ribbon simulation, not a 2D warp. Treat the bookmark as a ribbon 18 units long, pinned along its top edge (y = 3) and free at the tips (y = 21), and work it out edge on first. For progress p in 0..1 over 900ms (requestAnimationFrame): eased = 1 - (1 - p)^1.3; the crest position is c = -1.3s + (1 + 3.3s) * eased, where s is the spread (0.34 of the length), so it starts above the pin and ends past the tips. Depth toward the eye at hang h (0 at the pin, 1 at the tips), in grid units: z(h) = 8 * lift * settle(p) * h^0.85 * (bell(h - c, s) - echo * bell(h - c + 1.5s, 0.8s)), with bell(d, w) = exp(-(d / w)^2), lift 0.5, echo 0.35. The second bell is a shallower dip that follows the crest, so the ribbon falls back past flat before it rests. settle(p) is a smoothstep of (1 - p) / 0.32, so the swell dies away over the last third and the last frame is flat. Then keep the ribbon's length: sample z at 48 steps, and for each step advance the on-page height by sqrt(max(0.2, 1 - slope^2)) of the step, where slope is the change in z over the step's length. The tips ride up while the swell is on them. Finally project every outline point through a camera 12 units from the page looking at the icon's centre: scale = 12 / (12 - z), x' = 12 + (x - 12) * scale, y' = 12 + (Y - 12) * scale, where z and Y are read from the ribbon at that point's height. Lifted parts spread away from the centre, dipped parts pinch in, and the pinned top never moves. 3. The pop is transform only, 620ms, applied to a <g> wrapping the path inside the svg (transform-box: view-box, transform-origin: center), never to the svg element itself: a transform on the element can be composited from a bitmap taken at rest size, which goes soft as the pop grows, while the inner group is redrawn as a shape every frame. The keyframes: scale(1) at 0%, scale(1.45) rotate(-12deg) at 35%, scale(0.92) rotate(6deg) at 65%, none at 100%. Ease each leg with cubic-bezier(0.22, 1, 0.36, 1). With the Web Animations API put that easing on every keyframe and leave the animation's own easing linear; on the whole animation it would rush the first leg into a few frames. 4. The ring is a separate, pointer-transparent element exactly the row's box and radius (inset 0, so there is no gap between the row's edge and the ring): box-shadow from 0 0 0 0 (ink at 45%) to 0 0 0 10px transparent, 620ms ease-out. The row's own edge never animates. 5. The row's swell starts on a timer from the click, set as a share of the pop's duration (0.27, about 170ms), not when the pop ends: the two must overlap, and a slower pop must move the swell with it. 6. A second swell on the same icon restarts it. An unsave during a swell lets it finish. 7. With prefers-reduced-motion, change the state and the count and play nothing. 8. Announce the new count once (role="status" on the row). 9. The count is one slot per digit (tabular numerals), each slot a window one line tall, clipped top and bottom, and every slot the same width (about 0.84ch, digit centred) so a 1 becoming a 0 never moves its neighbour. On a change, match digits from the right and animate only the slots whose digit changed: the old digit leaves through the top over 300ms (translateY to -100%, fading) while the new one enters from the bottom over 420ms, both with cubic-bezier(0.22, 1, 0.36, 1). When the count shrinks the directions flip. The outgoing digit is a temporary aria-hidden element removed when it finishes. Expose in a DialKit panel: wave duration, lift, spread and echo; pop scale, tilt and duration; where in the pop the row's swell starts (0 to 1.5 of the pop); ring on or off and its size; the starting count; an overall size. Scale every dimension from one unit so the pair fits its container.
Append your stack, content, and brand constraints before handing it to your agent.
