A pottery studio sells gift cards in any amount, and the order form uses a slider. Most people flick it across the bar in one go, overshoot, and then nudge it back. This slider reacts to that rush the way glaze would: drag it too fast and fine white cracks open under the handle, a moment later they fill with gold, then they fade. Nothing is ever broken and the amount always updates. It just rewards a slow, careful hand with a calmer bar and gives the quick ones a small surprise.
Clay studio gift card, any amount from $25 to $300
One evening wheel class
How it works
- A real slider underneath. The control is a normal
<input type="range">with a clear track, sitting on top of a green bar drawn in CSS. Keyboards, screen readers and form submission all work exactly as they would with a plain range. - Measure the speed. On every
inputevent the script compares the new value with the last one and the time between them, giving a speed in track widths per second. - Draw a crack. When that speed passes
SPEED, the script adds a few short zigzag lines to an SVG layer, starting under the handle and spreading left and right. The lines are random each time, so no two cracks look the same. - Mend in gold. A class change after 450 milliseconds turns each line from off white to gold and makes it a little thicker. CSS transitions do the animation, so there is no animation loop running in the background.
- Clean up. After a couple of seconds the lines fade and are removed from the page, and the script caps how many can exist at once.
The code
Three self contained blocks: the markup, the styles scoped to the ks prefix, and a short script.
<div class="ks" data-lab="kintsugi-slider">
<label class="ks-q" for="ks-range">Gift card amount</label>
<p class="ks-sub">Clay studio gift card, any amount from $25 to $300</p>
<div class="ks-wrap">
<div class="ks-rail" aria-hidden="true"></div>
<svg class="ks-cracks" aria-hidden="true" focusable="false"></svg>
<input class="ks-range" id="ks-range" type="range" min="25" max="300" step="5" value="80" aria-describedby="ks-what">
</div>
<p class="ks-out"><output class="ks-amount" for="ks-range">$80</output> <span class="ks-what" id="ks-what">One evening wheel class</span></p>
<p class="ks-hint" aria-hidden="true">Drag gently. Rush it and the glaze cracks, then mends in gold.</p>
</div>.ks {
max-width: 460px;
margin: 0 auto;
padding: 24px 26px 20px;
border: 1px solid #c9cec4;
border-radius: 16px;
background: #fff;
color: #101a14;
font-family: system-ui, sans-serif;
line-height: 1.4;
box-sizing: border-box;
}
.ks p { margin: 0; } /* spacing uses padding, so page styles for p cannot change it */
.ks-q { display: block; font-size: 18px; font-weight: 700; color: #133621; }
.ks-sub { padding-top: 2px; font-size: 14px; color: #46534a; }
.ks-wrap { position: relative; height: 56px; margin-top: 14px; }
/* The glazed bar people see. The real input sits on top with a clear track. */
.ks-rail {
position: absolute;
left: 0; right: 0; top: 50%;
height: 26px;
margin-top: -13px;
border-radius: 13px;
background: linear-gradient(#1d4a2f, #133621);
box-shadow: inset 0 2px 3px rgba(0, 0, 0, .25);
}
.ks-cracks {
position: absolute;
left: 0; top: 50%;
width: 100%;
height: 26px;
margin-top: -13px;
border-radius: 13px;
overflow: hidden;
pointer-events: none;
}
.ks-cracks polyline {
fill: none;
stroke: #f2f3ef;
stroke-width: 1.6;
stroke-linecap: round;
stroke-linejoin: round;
transition: stroke .5s ease, stroke-width .5s ease, opacity .9s ease;
}
.ks-cracks .is-gold { stroke: #f4d96c; stroke-width: 2.4; }
.ks-cracks .is-gone { opacity: 0; }
/* Long selectors on purpose: they outrank range styles a site theme may already have. */
.ks .ks-wrap .ks-range[type=range] {
position: absolute;
left: 0; top: 0;
width: 100%;
height: 56px;
min-height: 0;
margin: 0;
padding: 0;
border: 0;
background: transparent;
-webkit-appearance: none;
appearance: none;
cursor: pointer;
}
.ks .ks-wrap .ks-range[type=range]::-webkit-slider-runnable-track { height: 56px; border: 0; background: transparent; }
.ks .ks-wrap .ks-range[type=range]::-moz-range-track { height: 56px; border: 0; background: transparent; }
.ks .ks-wrap .ks-range[type=range]::-moz-range-progress { background: transparent; }
.ks .ks-wrap .ks-range[type=range]::-webkit-slider-thumb {
-webkit-appearance: none;
box-sizing: border-box;
width: 36px; height: 36px;
margin-top: 10px;
border: 3px solid #fff;
border-radius: 50%;
background: #f4d96c;
box-shadow: 0 2px 6px rgba(16, 26, 20, .35);
}
.ks .ks-wrap .ks-range[type=range]::-moz-range-thumb {
box-sizing: border-box;
width: 36px; height: 36px;
border: 3px solid #fff;
border-radius: 50%;
background: #f4d96c;
box-shadow: 0 2px 6px rgba(16, 26, 20, .35);
}
.ks .ks-wrap .ks-range[type=range]:focus { outline: none; }
.ks .ks-wrap .ks-range[type=range]:focus-visible { outline: 3px solid #f4d96c; outline-offset: 4px; border-radius: 16px; }
.ks-out { padding-top: 10px; font-size: 15px; color: #46534a; }
.ks-amount { margin-right: 6px; font-size: 26px; font-weight: 800; color: #133621; }
.ks-hint { padding-top: 8px; min-height: 1.4em; font-size: 13px; color: #46534a; transition: color .3s; }
.ks-hint.is-on { color: #133621; font-weight: 600; }
@media (prefers-reduced-motion: reduce) {
.ks-cracks polyline, .ks-hint { transition: none; }
}(function () {
var root = document.querySelector('[data-lab="kintsugi-slider"]');
if (!root) return;
var range = root.querySelector('.ks-range');
var svg = root.querySelector('.ks-cracks');
var amount = root.querySelector('.ks-amount');
var what = root.querySelector('.ks-what');
var hint = root.querySelector('.ks-hint');
var NS = 'http://www.w3.org/2000/svg';
var SPEED = 1.4; // track widths per second that count as rushing
var THUMB = 36; // thumb width in px, so cracks start under it
var calm = window.matchMedia && matchMedia('(prefers-reduced-motion: reduce)').matches;
var lastV = +range.value, lastT = 0, lastCrack = 0, hintTimer = 0;
// Plain words for each price band, so the number means something.
function describe(v) {
if (v < 50) return 'A paint your own mug session';
if (v < 100) return 'One evening wheel class';
if (v < 200) return 'A wheel class for two';
return 'A four week beginner course';
}
// One crack: a few short zigzag branches spreading out from point x.
function crack(x) {
var w = svg.clientWidth, h = svg.clientHeight;
svg.setAttribute('viewBox', '0 0 ' + w + ' ' + h);
var lines = [], branches = 3 + Math.floor(Math.random() * 3);
for (var b = 0; b < branches; b++) {
var px = x, py = h / 2 + (Math.random() - 0.5) * 6;
var dir = (b % 2 ? 1 : -1) * (0.4 + Math.random());
var pts = [px.toFixed(1) + ',' + py.toFixed(1)];
for (var s = 0; s < 5; s++) {
px += dir * (5 + Math.random() * 9);
py += (Math.random() - 0.5) * 12;
pts.push(px.toFixed(1) + ',' + Math.max(1, Math.min(h - 1, py)).toFixed(1));
}
var line = document.createElementNS(NS, 'polyline');
line.setAttribute('points', pts.join(' '));
svg.appendChild(line);
lines.push(line);
}
// White fissure first, gold seam a moment later, then it fades away.
setTimeout(function () { mark(lines, 'is-gold'); }, calm ? 0 : 450);
setTimeout(function () { mark(lines, 'is-gone'); }, calm ? 1600 : 2600);
setTimeout(function () { lines.forEach(function (l) { svg.removeChild(l); }); }, calm ? 1700 : 3600);
}
function mark(lines, cls) {
lines.forEach(function (l) { l.setAttribute('class', cls === 'is-gone' ? 'is-gold is-gone' : cls); });
}
function update() {
var v = +range.value, now = Date.now();
var span = range.max - range.min;
amount.textContent = '$' + v;
what.textContent = describe(v);
// Speed in track widths per second since the last change.
var dt = Math.max(16, now - lastT);
var speed = Math.abs(v - lastV) / span / (dt / 1000);
if (lastT && speed > SPEED && now - lastCrack > 140 && svg.childNodes.length < 40) {
lastCrack = now;
var frac = (v - range.min) / span;
crack(THUMB / 2 + frac * (range.clientWidth - THUMB));
hint.classList.add('is-on');
clearTimeout(hintTimer);
hintTimer = setTimeout(function () { hint.classList.remove('is-on'); }, 2200);
}
lastV = v;
lastT = now;
}
range.addEventListener('input', update);
range.addEventListener('pointerdown', function () { lastT = 0; lastV = +range.value; });
update();
})();When to use it, and when not to
It fits a business whose story already involves craft and patience: a ceramics studio, a glass blower, a furniture restorer, a tea shop, a bookbinder. On a gift card or a class budget selector it adds a small moment that matches the brand, and the gold seams carry a pleasant idea of repair rather than damage. It is a detail people discover by accident, which is part of the charm.
It is a poor fit where the message would feel wrong. On an insurance quote, a medical booking or a donation form, cracks suggest that something went wrong, and nobody wants that feeling next to a payment. It also adds nothing on a slider people move once with care, because they will never see it.
On accessibility, the effect is purely decorative and marked aria-hidden, so screen reader users get a normal labelled slider with the price band read as its description. Keyboard steps are small and slow, so they never trigger cracks. People who ask their system for reduced motion still see the gold seams, but without the fade from white and with a shorter life. Never let an effect like this block or change the value; if it did, it would stop being playful and start being a trap.
Make it yours
- How easily it cracks: raise
SPEEDto make cracks rarer, lower it to make them appear on almost any quick drag. - Your prices: set
min,max,stepandvalueon the input, and rewrite the bands insidedescribe()to match what you sell. - Colours and timing: the bar is the gradient on
.ks-rail, the seam colour is the stroke on.ks-cracks .is-gold, and the twosetTimeoutdelays incrack()control when the gold arrives and when it fades.
Frequently asked questions
Does the amount still work if the slider cracks?
Yes. The cracks are drawn on a separate layer behind the handle and never touch the input, so the value updates on every move and submits with the form like any range input.
Does it work on phones?
Yes. It is a native range input, so touch dragging works out of the box, and a quick swipe of the handle is enough to see the effect.
Can I use it with a different control?
The speed check and the crack() function only need a position and a number that changes over time, so the same idea works on a custom slider, a quantity stepper or a progress bar with a little adaptation.


