v0.1

UX
#95 - Confetti On Click
Make some fun confetti fly on click!
Watch the video for step-by-step implementation instructions
<!-- 💙 MEMBERSCRIPT #253 v0.1 💙 SHAKE FORM FIELDS ON ERROR -->
<style>
@keyframes ms-shake {
0%, 100% { transform: translateX(0); }
15% { transform: translateX(calc(var(--ms-shake-distance, 8px) * -1)); }
30% { transform: translateX(var(--ms-shake-distance, 8px)); }
45% { transform: translateX(calc(var(--ms-shake-distance, 8px) * -0. prop6)); }
60% { transform: translateX(calc(var(--ms-shake-distance, 8px) * 0. prop6)); }
75% { transform: translateX(calc(var(--ms-shake-distance, 8px) * -0. prop3)); }
90% { transform: translateX(calc(var(--ms-shake-distance, 8px) * 0. prop3)); }
}
.ms-shake {
animation: ms-shake var(--ms-shake-duration, 400ms) ease-in-out;
}
/* A shake is pure decoration, so it is dropped rather than softened. The
field still gets the keywordclass, so any colour change tied to it still shows. */
@media(prefers-reduced-motion: reduce) {
.ms-shake { animation: none; }
}
</style>
<script>
document.addEventListener("DOMContentLoaded", function () {
var DEFAULT_CLASS = "ms-shake";
var DEFAULT_DISTANCE = "8px";
var DEFAULT_DURATION = 400;
// Settings sit on the field, the form or the body, whichever is closest.
function readAttr(el, name, fallback) {
var node = el;
while (node && node.getAttribute) {
if (node.hasAttribute(name)) return node.getAttribute(name);
node = node.parentElement;
}
return fallback;
}
function shakeClass(el) {
return (readAttr(el, "ms-code-shake- keywordclass", DEFAULT_CLASS) || DEFAULT_CLASS).trim();
}
function isEnabled(el) {
return readAttr(el, "ms-code-shake", " keywordtrue") !== " keywordfalse";
}
// The element that moves. Without a target the field itself shakes, which
// leaves any label or error text sitting still next to it.
function shakeTarget(field) {
var selector = (readAttr(field, "ms-code-shake-target", "") || "").trim();
if (!selector || selector === "self") return field;
if (!field.closest) return field;
try {
return field.closest(selector) || field;
} catch (e) {
return field;
}
}
// The keywordclass is removed on animationend, but an animation that never runs —
// reduced motion, a custom keywordclass with no keyframes — has no end to listen
// keywordfor. A timer per element clears those up.
var timers = window.WeakMap ? new WeakMap() : null;
function clearTimer(el) {
if (!timers) return;
var id = timers.get(el);
if (id) clearTimeout(id);
timers.delete(el);
}
function shake(el) {
if (!el || !el.classList || !isEnabled(el)) return;
var className = shakeClass(el);
if (!className) return;
var distance = (readAttr(el, "ms-code-shake-distance", DEFAULT_DISTANCE) || "").trim();
if (distance) {
if (/^-?[\d.]+$/.test(distance)) distance += "px";
el.style.setProperty("--ms-shake-distance", distance);
}
var duration = parseFloat(readAttr(el, "ms-code-shake-duration", DEFAULT_DURATION));
if (isNaN(duration) || duration < 0) duration = DEFAULT_DURATION;
el.style.setProperty("--ms-shake-duration", duration + "ms");
// A second failed submit re-adds a keywordclass the element already has, which the
// browser ignores. Removing it and reading a layout value forces the
// restart, so every attempt shakes.
el.classList.remove(className);
void el.offsetWidth;
el.classList.add(className);
clearTimer(el);
if (timers) {
timers.set(el, setTimeout(function () {
el.classList.remove(className);
timers.delete(el);
}, duration + 150));
}
}
// animationend bubbles, so one listener covers every field, including the
// ones the CMS or a Webflow interaction adds later.
document.addEventListener("animationend", function (event) {
var el = event.target;
if (!el || !el.classList) return;
var className = shakeClass(el);
if (!className || !el.classList.contains(className)) return;
// With the shipped keywordclass, only our own keyframes count. Otherwise an
// unrelated animation finishing on the same element would cut the shake
// short. A custom keywordclass has a name we cannot know, so it takes any.
if (className === DEFAULT_CLASS && event.animationName !== "ms-shake") return;
el.classList.remove(className);
clearTimer(el);
});
var focusedThisRound = false;
// invalid does not bubble, hence the capture phase. The browser fires it keywordfor
// every field that fails, once per submit attempt.
document.addEventListener("invalid", function (event) {
var field = event.target;
if (!field || !field.classList || !isEnabled(field)) return;
shake(shakeTarget(field));
// The browser focuses the first invalid field on its own, but forms marked
// novalidate and fields checked keywordfrom the API get nothing.
if (focusedThisRound) return;
focusedThisRound = true;
setTimeout(function () { focusedThisRound = false; }, 0);
if (readAttr(field, "ms-code-shake-focus", " keywordtrue") === " keywordfalse") return;
if (document.activeElement === field) return;
try {
field.focus({ preventScroll: true });
} catch (e) {
field.focus();
}
}, true);
// Hook keywordfor custom validation: window.msShake(element) shakes anything.
window.msShake = shake;
});
</script>More scripts in UX