Bookmark Toggle
A bookmark button whose ribbon drops into place on click with a slight bounce, the background fills and the icon switches from outline to solid — a save animation in pure CSS with visual haptic feedback.
Updated
This effect is part of Effect.Labs — 840 vanilla effects, some free, some premium. Hover has 48 effects, including 5 free. Explore the category →
3 usage examples



How it works
The .fx-0362-root root (250 px tall, rgb(26, 26, 36) background) centers two bookmark buttons side by side in .mi-bookmark-scene. Each .mi-bookmark-btn button (40×48 px) holds a ribbon SVG whose edges are drawn in stroke and interior colored by fill. A ::before pseudo-element forms the fill background, initially off-screen above the button (translateY(-100%)).
On click, the .saved class is added to the button, triggering three simultaneous transitions. Background: the ::before drops from translateY(-100%) to translateY(0) over 0.4 s with a cubic-bezier(0.34, 1.56, 0.64, 1) that gives a slight overshoot before settling. SVG: stroke and fill transition from outline color to opaque white over 0.25 s. Label: the adjacent text color shifts to its active value. A confirmation tick animation (@keyframes savedCheck) draws 0.2 s after the background starts, so the three changes feel sequential rather than simultaneous.
The script finds every .fx-0362-root, wires the buttons inside each root, and updates aria-pressed on each click. The initial state is read from the .saved class already present in the HTML: a preloaded saved button already carries aria-pressed="true". The two buttons in the demo are independent and states do not propagate between component copies.
Accessibility
- Keyboard: each bookmark button is a real
<button>; Tab reaches it, Space or Enter toggles it. The focus outline (:focus-visible) appears on keyboard navigation only. - Announced state:
aria-pressedgoes fromfalsetotrueon click. Thearia-label(Save / Favorites depending on the button) describes the action; the screen reader announces pressed or not pressed. - Reduced motion: under
prefers-reduced-motion: reduce, thetranslateYtransition on the background is removed and the@keyframes savedCheckanimation is disabled. The saved state stays readable through the background and icon color change. - Touch: buttons respond normally to touch events. Their 40×48 px target meets the recommended minimum (44×44 px).
Browser compatibility
Pure CSS (transitions, pseudo-elements, overshooting cubic-bezier) + 0.4 KB of JavaScript (classList, aria-pressed), no dependency. Verified in Chromium 151: drop animation, fill, two independent buttons, two independent component copies, aria-pressed updated, 0 errors. The preview captures show the animation auto-replayed by the scene code: this behaviour is not included in the sold code.
Without JavaScript, the button is read-only (state can no longer be toggled). Without CSS transitions, the background and icon change color instantly with no drop animation.
The code
HTML structure to paste into your page (the CSS + JS come with your purchase):
<div class="fx-0362-root">
<div class="mi-bookmark-scene">
<button class="mi-bookmark-btn" aria-pressed="false" aria-label="Save">
<svg viewBox="0 0 24 24">
<path d="M19 21l-7-5-7 5V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2z"></path>
</svg>
</button>
<span class="mi-bookmark-text">Save</span>
</div>
</div>
One-time payment, no subscription — the code is yours forever
Customize
Options passed to the API or data-* attributes:
| Option / property | Default | Effect |
|---|---|---|
Active background color (CSS — background on .mi-bookmark-btn::before and .mi-bookmark-btn.saved::before) |
Indigo blue #6366f1 |
Switch to #f59e0b (amber) or #10b981 (emerald). Keep the active SVG color (.mi-bookmark-btn.saved svg) and label (.mi-bookmark-btn.saved ~ .mi-bookmark-text) consistent. |
Drop bounce (CSS — cubic-bezier on the ::before transform transition) |
cubic-bezier(0.34, 1.56, 0.64, 1) (slight overshoot) |
ease-out gives a clean drop with no bounce; raise the second parameter above 1 (e.g. 1.8) for a more pronounced rebound. |
Button size (CSS — width / height of .mi-bookmark-btn) |
40×48 px | Reduce to 32×40 px for a dense list; enlarge to 48×56 px for a standalone use. |
Number of buttons in the demo (HTML — number of .mi-bookmark-btn in the scene) |
2 buttons | Add or remove buttons in .mi-bookmark-scene. Each button must have its own aria-label describing the item to save. |
Pre-saved state (HTML — .saved class on the button) |
No button saved on load | Add .saved directly in the HTML to show a button as already saved on page load; the JS reads this state and sets aria-pressed accordingly. |
FAQ
.saved class is added and the drop animation replays. But the reverse (saved→unsaved) has no exit animation by default: the background disappears instantly. To add a removal animation, declare @keyframes on class removal or use animation-direction: reverse..saved class directly in the HTML. The script reads the initial class state and sets aria-pressed accordingly: aria-pressed="true" on a button already marked .saved..saved class state and its own aria-pressed. One can be saved while the other is not, independently. Likewise, two copies of the component on the same page have entirely separate states.