GODRICH
#css-animation#button#loader#transition#scroll#dashboard#ui#design-trends

Slide to Unlock Button: 9 Slider Scenes

In a slide to unlock button the release decides everything: 02 adds speed to a short release, 03 springs back when it falls short, and 08 measures progress by

Slide to Unlock Is Measuring a Position, Not Pressing a Button: a Slide to Unlock Button Starts by Turning the Handle Position Into a Progress Between 0 and 1

Auto-plays · click a tile to jump to its section · all nine in one zip

One handle position drives every other value

Each of the nine handles has role="slider", and its position is one number, sl.p, that starts at 0 and ends at 1. Fill, label, color and shadow are all computed from that number, the handle and the fill end move with translateX(), and the label, cards and ring move with opacity and transform. Only 08 draws an SVG arc, and the rest are plain DOM.

The verdict is taken only at release

While the handle is held, nothing decides whether the slide counts. At release release() calls verdict(p, vEma) once, then hangs a spring toward the end if it passed or toward the start if not. 04 is the one exception, because it hands over to completion the moment the threshold is crossed even while held.

How this differs from the neighboring posts

The button press physics post covers how a press value returns, and the card swipe post covers dragging a card away. This post differs in that the handle has to travel to the end before one result is chosen, and reading the hand through arithmetic puts it next to the virtual joystick post. A swipe to unlock button that only fires on a full push also has a cousin in the send button post, where the press itself is the gesture.

Slide to Unlock Button: 9 Scenes — a Rubber Track That Gets Heavier as You Pull, a Shimmer Label That Sweeps the Text, a Spring Return When You Fall Short, a Threshold Auto Finish, a Flick Verdict That Adds Speed, a Slide to Confirm Payment Step, Hint Chevrons That Ripple, an Arc Power Slider and a Lock Screen Hero That Sharpens

01 puts resistance on a finger that pulls past the end, and 02 to 04 split the release verdict into speed, spring and threshold. 05 to 07 fill the space around the handle with a label, color and a hint, 08 drops the straight line for an arc, and 09 moves a whole background from the same single value.

What the nine scenes measured

The 2-second stage of every slider was captured as 24 frames, so 23 differences between neighbors are counted per scene.

Scene Area covered Frames that moved Mean intensity
01 Rubber track 19.1% 14/23 83.1
02 Flick verdict 14.5% 14/23 61.2
03 Spring return 12.3% 18/23 99.0
04 Threshold auto finish 18.6% 23/23 70.4
05 Shimmer label 13.6% 17/23 78.4
06 Payment stages 15.7% 21/23 109.5
07 Hint chevrons 12.7% 18/23 83.3
08 Arc slider 19.8% 16/23 69.8
09 Lock screen 40.1% 15/23 92.5

The widest change is 40.1% in 09, where scenery, clock and cards all move from one value, and the narrowest is 12.3% in 03. 04 moved in all 23 frames because the ring, check and track keep changing after completion, while 01 and 02 moved in 14 of 23. Mean intensity is highest in 06 at 109.5 and lowest in 02 at 61.2.

01A Rubber Track That Gets Heavier to Pull Near the End

Pull the finger to 1.3 near the end and mapDrag sends the handle only to 1.078, while the overflow stretches a rubber band that thins by 1/(1+over·BAND_THIN). Up to 0.78 (KNEE) the handle follows the finger one to one, and past it a decaying exponential gives less and less, so the hand feels weight near the end. Use it as the base screen of a payment or delete confirmation, where a finger that overshoots must not throw the handle off the track.

A Rubber Track That Gets Heavier to Pull Near the End
Open in a new tab

01 · A Rubber Track That Gets Heavier to Pull Near the End — Measured area 19.147% · moving frames 14/23Original by GODRICH · SCSS and React ship in the zip · See the password

KNEESTRETCHmapDrag
    // finger value x to handle progress: one to one up to KNEE, then less and less as it nears the end
    function mapDrag(x) {
        if (x <= KNEE) {
            return Math.max(x, 0);
        }
        const span = 1 + STRETCH - KNEE;
        return KNEE + span * (1 - Math.exp(-(x - KNEE) / span));
    }
    function draw() {
        placeHandle(main, sl.p);
        const over = Math.max(0, sl.p - 1);
        const w = over > 0 ? 4 - PAD_CQW + over * travelCqw() : 0;
        band.style.transform = 'scaleX(' + (w / BAND_W).toFixed(3) + ') scaleY(' + (1 / (1 + over * BAND_THIN)).toFixed(3) + ')';
        band.style.opacity = over > 0 ? '1' : '0';
        // where the finger really is: its distance from the handle is the resistance
        ring.style.transform = 'translateX(' + (xOf(sl.fx) - 3).toFixed(2) + 'cqw)';
        ring.style.opacity = sl.down ? clamp((sl.fx - sl.p - 0.06) / 0.06, 0, 1).toFixed(3) : '0';
        placeHandle(mini, badX);
    }

02A Flick Verdict That Adds Speed So a Short Release Still Finishes

At release landing(p, v) = p + max(v, 0)·FLICK_T projects a landing point, and a projection of 0.80 or more glides the remaining distance to finish. In the demo the fast hand lets go at position 0.503 and speed 1.35, projects 0.84 and finishes at 1.18 s, while the slow hand reaches 0.543 at speed 0.39, projects 0.64 and goes back. It suits an unlock that has to accept a light flick, and the dotted ghost shows the projected landing before the finger lifts.

A Flick Verdict That Adds Speed So a Short Release Still Finishes
Open in a new tab

02 · A Flick Verdict That Adds Speed So a Short Release Still Finishes — Measured area 14.492% · moving frames 14/23Original by GODRICH · SCSS and React ship in the zip · See the password

FLICK_TlandingTHRESH
    // scene 02 has no rubber band: the finger value is only clipped between 0 and 1
    function mapDrag(x) { return clamp(x, 0, 1); }
    // projected landing: position + speed × FLICK_T, and backward speed is not added
    function landing(p, v) { return p + Math.max(v, 0) * FLICK_T; }
    function verdict(p, v) { return landing(p, v) >= THRESH; }
    // demo finger: even cycles sweep fast and let go short, odd cycles drag slowly to the same spot and let go
    function fingerAt(t, c) {
        if (c % 2 === 0) {
            return { down: t >= 0.2 && t < 0.6, x: ramp(t, [[0.2, 0], [0.6, 0.55]]) };
        }
        return { down: t >= 0.2 && t < 1.6, x: ramp(t, [[0.2, 0], [1.6, 0.55]]) };
    }
    function tick(h) {
        void h;
        if (sl.down) {
            land = landing(sl.p, sl.vEma);
        }
    }
    function draw() {
        placeHandle(main, sl.p);
        // dotted ghost: while held it is the landing projected from the current speed, after release it stays and fades over 1.2 s
        const since = sl.relT < 0 ? 0 : sceneT - sl.relT;
        const keep = sl.down ? 1 : (sl.relT < 0 ? 0 : clamp(1 - since / 1.2, 0, 1));
        const shown = land;
        const hit = shown >= THRESH;
        ghost.style.transform = 'translateX(' + (clamp(shown, 0, 1.05) * travelCqw()).toFixed(3) + 'cqw)';
        ghost.style.opacity = (keep * (shown > 0.05 ? 1 : 0)).toFixed(3);
        ghost.classList.toggle(cls('is-hit'), hit);
        const txt = shown <= 0.05 ? '' : (hit ? 'Projected landing {n}%: past the threshold, so it finishes' : 'Projected landing {n}%: short of the threshold, so it goes back');
        note.textContent = txt.replace('{n}', String(Math.round(shown * 100)));
        note.style.opacity = txt ? '1' : '0';
    }

03A Spring Return That Overshoots Back When the Slide Falls Short

Let go short of the 0.80 threshold and v += (−SPR_K·p − SPR_C·v)·h sends the handle home on a damped spring, past the start and back. In the demo a release at 0.525 crosses the start twice, dips to −0.117 at the lowest and settles at the start 1.17 s after release. Use it on a lock screen that must recover safely from an accidental touch, and watch the contrast version below it, where the fill stays half full.

A Spring Return That Overshoots Back When the Slide Falls Short
Open in a new tab

03 · A Spring Return That Overshoots Back When the Slide Falls Short — Measured area 12.287% · moving frames 18/23Original by GODRICH · SCSS and React ship in the zip · See the password

SPR_KSPR_CTHRESH
        else if (sl.mode === M_BACK) {
            sl.v += (-SPR_K * sl.p - SPR_C * sl.v) * h;
            sl.p += sl.v * h;
            if (Math.abs(sl.p) < 0.002 && Math.abs(sl.v) < 0.03) {
                sl.p = 0;
                sl.v = 0;
                sl.mode = M_IDLE;
            }
        }
        else if (sl.mode === M_FIN) {
            sl.v += (FIN_K * (1 - sl.p) - FIN_C * sl.v) * h;
            sl.p += sl.v * h;
            if (Math.abs(1 - sl.p) < 0.002 && Math.abs(sl.v) < 0.05) {
                sl.p = 1;
                sl.v = 0;
                sl.mode = M_DONE;
                sl.doneT = 0;
            }
        }
    function draw() {
        placeHandle(main, sl.p);
        placeHandle(mini, badP);
        // show how far the spring went past the start (progress below 0) as text
        const under = Math.max(0, -sl.p);
        const txt = under > 0.004 ? 'Passed the start by {n}% and coming back' : '';
        note.textContent = txt.replace('{n}', String(Math.round(under * 100)));
        note.style.opacity = txt ? '1' : '0';
    }

04A Threshold Auto Finish That Carries the Rest and Turns Into a Check

When progress touches THRESH 0.6 even while the finger is down, autoCross hands over to M_FIN and the handle carries the rest of the way by itself. In the demo it reaches 0.60 at 1.03 s, shows the done state at 1.82 s, and from there the arrow turns into a check, one ring spreads and the track fades. It fits a confirm step that should not make the user hold to the end.

A Threshold Auto Finish That Carries the Rest and Turns Into a Check
Open in a new tab

04 · A Threshold Auto Finish That Carries the Rest and Turns Into a Check — Measured area 18.602% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

THRESHautoCrossRING_S
        if (sl.mode === M_DRAG) {
            const old = sl.p;
            sl.p += (mapDrag(f.x) - sl.p) * (1 - Math.exp(-h * FOLLOW));
            sl.vEma += ((sl.p - old) / h - sl.vEma) * (1 - Math.exp(-h * VEL_SMOOTH));
            if (autoCross && sl.p >= THRESH) {
                sl.down = false;
                sl.relP = sl.p;
                sl.relV = sl.vEma;
                sl.relT = sceneT;
                sl.ok = true;
                sl.v = sl.vEma;
                sl.mode = M_FIN;
            }
        }
    function draw() {
        placeHandle(main, sl.p);
        const t = sl.mode === M_DONE ? sl.doneT : -1;
        const u = t < 0 ? 0 : clamp(t / CHECK_S, 0, 1);
        const arrow = main.ico;
        arrow.style.opacity = (1 - u).toFixed(3);
        check.style.opacity = u.toFixed(3);
        check.style.transform = 'scale(' + (0.6 + 0.4 * easeOut3(u)).toFixed(3) + ')';
        // ring: grows once behind the handle and fades
        const r = t < 0 ? 0 : clamp(t / RING_S, 0, 1);
        pulse.style.transform = 'translateX(' + (travelCqw()).toFixed(3) + 'cqw) scale(' + (1 + 1.3 * easeOut3(r)).toFixed(3) + ')';
        pulse.style.opacity = t < 0 ? '0' : (0.7 * (1 - r)).toFixed(3);
        // after completion the whole track fades
        const f = t < 0 ? 0 : easeInOut((t - FADE_AT_S) / 0.6);
        main.bg.style.opacity = (1 - 0.45 * f).toFixed(3);
        const txt = t >= 0 ? 'Unlocked' : 'Slide to unlock';
        if (main.label.textContent !== txt) {
            main.label.textContent = txt;
        }
    }

05A Guide Label That Fades Where the Handle Passes While a Shimmer Sweeps It

When idle a bright band crosses the letters from left to right every SHIM_PERIOD 1.6 s, and a letter the handle has passed turns transparent by (handle right edge − letter center)/FADE_W. measure() reads the centers of the nine letters once with getBoundingClientRect and again only when the width changes, and a touch switches the shimmer off through idle = 1 − p/0.12. Use it to make a button hint stand out while erasing the words already passed.

A Guide Label That Fades Where the Handle Passes While a Shimmer Sweeps It
Open in a new tab

05 · A Guide Label That Fades Where the Handle Passes While a Shimmer Sweeps It — Measured area 13.636% · moving frames 17/23Original by GODRICH · SCSS and React ship in the zip · See the password

SHIM_PERIODSHIM_WFADE_W
    function measure() {
        const w = rootW();
        if (w === measuredW) {
            return;
        }
        measuredW = w;
        const tr = main.track.getBoundingClientRect();
        const k = 100 / w;
        charX = [];
        for (let i = 0; i < chars.length; i += 1) {
            const r = chars[i].getBoundingClientRect();
            charX.push((r.left + r.width / 2 - tr.left) * k);
        }
        const first = chars[0].getBoundingClientRect();
        const last = chars[chars.length - 1].getBoundingClientRect();
        lineL = (first.left - tr.left) * k;
        lineW = Math.max(1, (last.right - first.left) * k);
    }
    function draw() {
        measure();
        placeHandle(main, sl.p);
        const xr = PAD_CQW + sl.p * travelCqw() + HANDLE_CQW;
        // center of the shimmer band (ratio of the line length): one sweep follows another without a rest, and a touch fades it out
        const idle = 1 - clamp(sl.p / 0.12, 0, 1);
        const s = ((sceneT / SHIM_PERIOD) % 1) * (1 + 2 * SHIM_W) - SHIM_W;
        for (let i = 0; i < chars.length; i += 1) {
            const f = (charX[i] - lineL) / lineW;
            const gain = Math.max(0, 1 - Math.abs(f - s) / SHIM_W) * idle;
            const gone = clamp((xr - charX[i]) / FADE_W, 0, 1);
            chars[i].style.opacity = ((BASE_ALPHA + (1 - BASE_ALPHA) * gain) * (1 - gone)).toFixed(3);
            chars[i].style.transform = 'scale(' + (1 + 0.1 * gain).toFixed(3) + ')';
        }
        const sx = lineL + s * lineW - 10;
        sheen.style.transform = 'translateX(' + sx.toFixed(2) + 'cqw) skewX(-20deg)';
        sheen.style.opacity = (0.6 * idle).toFixed(3);
    }

06A Payment Confirm Slider Whose Color and Amount Copy Change in Stages as You Slide

As progress crosses STAGE_1 0.33 and STAGE_2 0.7 the fill goes from lilac to sky to mint and the amount copy changes in three steps, with colors blending only across a BLEND_W 0.12 band at each border. After completion a loading ring spins for SPIN_S 0.9 s with a processing line and then closes into a check with a done line. Use it for transfer or order steps that must prevent an accidental payment.

A Payment Confirm Slider Whose Color and Amount Copy Change in Stages as You Slide
Open in a new tab

06 · A Payment Confirm Slider Whose Color and Amount Copy Change in Stages as You Slide — Measured area 15.686% · moving frames 21/23Original by GODRICH · SCSS and React ship in the zip · See the password

STAGE_1STAGE_2stageColor
    // progress to fill color: the three stage colors blend only across a BLEND_W band at each border
    function stageColor(p) {
        const a = easeInOut((p - STAGE_1 + BLEND_W / 2) / BLEND_W);
        const b = easeInOut((p - STAGE_2 + BLEND_W / 2) / BLEND_W);
        const c = [0, 0, 0];
        for (let i = 0; i < 3; i += 1) {
            const m1 = lerp(COLORS[0][i], COLORS[1][i], a);
            c[i] = Math.round(lerp(m1, COLORS[2][i], b));
        }
        return 'rgb(' + c[0] + ',' + c[1] + ',' + c[2] + ')';
    }
    function stageText(p) {
        if (p >= STAGE_2) {
            return '{a} release to pay'.replace('{a}', AMOUNT);
        }
        if (p >= STAGE_1) {
            return '{a} a bit more'.replace('{a}', AMOUNT);
        }
        return '{a} slide to confirm'.replace('{a}', AMOUNT);
    }
    function draw() {
        placeHandle(main, sl.p);
        placeHandle(mini, badP);
        const p = clamp(sl.p, 0, 1);
        const t = sl.mode === M_DONE ? sl.doneT : -1;
        main.fill.style.background = stageColor(p);
        // right after completion a loading ring spins, and when it ends it closes into a check
        const spin = t >= 0 && t < SPIN_S;
        const ok = t >= SPIN_S;
        spinner.style.opacity = spin ? '1' : '0';
        spinner.style.transform = 'rotate(' + ((t / SPIN_TURN_S) * 360).toFixed(1) + 'deg)';
        main.ico.style.opacity = t >= 0 ? '0' : '1';
        check.style.opacity = ok ? '1' : '0';
        check.style.transform = 'scale(' + (ok ? 0.6 + 0.4 * easeOut3((t - SPIN_S) / 0.25) : 0.6).toFixed(3) + ')';
        const txt = ok ? 'Payment done' : (spin ? 'Paying' : stageText(p));
        if (main.label.textContent !== txt) {
            main.label.textContent = txt;
        }
    }

07A Hint Where Chevrons Ripple in a Wave to Show the Direction When Left Alone

While untouched, three chevrons brighten in turn with a HINT_PHASE 0.16 offset, and the handle nudges forward by HINT_AMP·sin² of 0.14 and back every HINT_P 1.2 s. A touch changes mode and the hint goes out at once, and after a release it waits HINT_RESUME_S 0.6 s before coming back. It belongs in onboarding that must tell a first-time user which way to push, without words.

A Hint Where Chevrons Ripple in a Wave to Show the Direction When Left Alone
Open in a new tab

07 · A Hint Where Chevrons Ripple in a Wave to Show the Direction When Left Alone — Measured area 12.687% · moving frames 18/23Original by GODRICH · SCSS and React ship in the zip · See the password

HINT_PHINT_PHASEHINT_AMP
    // hint is on: the handle sits at the start and is neither held nor just released
    function hintOn() {
        return sl.mode === M_IDLE && sl.p < 0.001 && (sl.relT < 0 || sceneT - sl.relT > HINT_RESUME_S);
    }
    function draw() {
        const on = hintOn();
        const ph = (sceneT / HINT_P) % 1;
        // the handle nudges forward and back along a sine curve (sin squared)
        const hint = on ? HINT_AMP * Math.pow(Math.sin(Math.PI * ph), 2) : 0;
        placeHandle(main, sl.p + hint);
        for (let i = 0; i < chevs.length; i += 1) {
            const w = Math.sin(2 * Math.PI * (sceneT / HINT_P - i * HINT_PHASE));
            const a = on ? 0.22 + 0.78 * Math.pow(Math.max(0, w), 2) : 0.22;
            chevs[i].style.opacity = a.toFixed(3);
        }
    }

08A Power Slider That Turns Along an Arc and Measures Progress by Angle

The handle rides a half circle through arcX and arcY, and angleP reads the angle from the center with atan2 and turns it into progress as 1 − angle/π. The threshold is THRESH_DEG 144°, and a release past it makes the whole arc flash once with a FLASH_TAU 0.18 s decay while the power icon dims and the copy changes to the powered-off line. Use it for risky actions such as power off or an emergency stop.

A Power Slider That Turns Along an Arc and Measures Progress by Angle
Open in a new tab

08 · A Power Slider That Turns Along an Arc and Measures Progress by Angle — Measured area 19.829% · moving frames 16/23Original by GODRICH · SCSS and React ship in the zip · See the password

THRESH_DEGanglePFLASH_TAU
    // a point on the arc: progress p runs from the left end (180 degrees) to the right end (0 degrees), angle from the center θ = π(1 − p)
    function arcX(p) { return CX + R * Math.cos(Math.PI * (1 - p)); }
    function arcY(p) { return CY - R * Math.sin(Math.PI * (1 - p)); }
    // pointer position to progress: measure the angle from the center as polar coordinates, and snap anything below the arc to the nearer end
    function angleP(e) {
        const r = scene.getBoundingClientRect();
        const ctrX = r.left + r.width * CX / 100;
        const ctrY = r.top + r.height * CY / 62.5;
        let a = Math.atan2(ctrY - e.clientY, e.clientX - ctrX);
        if (a < -Math.PI / 2) {
            a = Math.PI;
        }
        else if (a < 0) {
            a = 0;
        }
        return 1 - a / Math.PI;
    }
    function draw() {
        const p = sl.p;
        handle.style.transform = 'translate(' + (arcX(p) - HANDLE_CQW / 2).toFixed(3) + 'cqw,' + (arcY(p) - HANDLE_CQW / 2).toFixed(3) + 'cqw)';
        // the arrow in the handle faces the arc tangent: up at the left end, right at the top, down at the right end
        arrow.style.transform = 'rotate(' + ((p - 0.5) * 180).toFixed(1) + 'deg)';
        const shown = clamp(p, 0, 1);
        fillArc.style.strokeDasharray = (ARC_LEN * shown).toFixed(2) + ' ' + ARC_LEN.toFixed(2);
        const off = sl.mode === M_DONE;
        const t = off ? sl.doneT : 0;
        flashArc.style.opacity = off ? (0.9 * Math.exp(-t / FLASH_TAU)).toFixed(3) : '0';
        fillArc.classList.toggle(cls('is-off'), off);
        // the center power icon dims once progress passes half and is faintest after power off
        const dim = easeInOut((shown - 0.5) / 0.5);
        power.style.opacity = (1 - 0.65 * dim).toFixed(3);
        const txt = off ? 'Power off' : 'Power on';
        if (powerLabel.textContent !== txt) {
            powerLabel.textContent = txt;
        }
    }

09A Lock Screen Hero Where the Background Sharpens and the Clock Retreats as You Slide

From one value p, blur(BLUR_MAX·(1−p)) clears the scenery, the clock rises CLOCK_RISE 9 cqw and shrinks down to CLOCK_MIN 0.5, and two notification cards climb in from CARD_FROM 0.3 with a CARD_GAP 0.15 step. Releasing back runs the same formulas in reverse, so every value returns along the path it came. Use it as the hero of an app or kiosk first screen or a service landing page.

A Lock Screen Hero Where the Background Sharpens and the Clock Retreats as You Slide
Open in a new tab

09 · A Lock Screen Hero Where the Background Sharpens and the Clock Retreats as You Slide — Measured area 40.138% · moving frames 15/23Original by GODRICH · SCSS and React ship in the zip · See the password

BLUR_MAXCLOCK_MINCARD_FROM
    function draw() {
        placeHandle(main, sl.p);
        const p = clamp(sl.p, 0, 1);
        // scenery blurs in inverse proportion to progress, and the clock rises, shrinks and fades: all from one value (p)
        img.style.filter = 'blur(' + (BLUR_MAX * (1 - p)).toFixed(3) + 'cqw)';
        clock.style.transform = 'translateY(' + (-CLOCK_RISE * p).toFixed(3) + 'cqw) scale(' + lerp(1, CLOCK_MIN, p).toFixed(3) + ')';
        clock.style.opacity = (1 - 0.3 * p).toFixed(3);
        for (let i = 0; i < cards.length; i += 1) {
            const u = easeOut3((p - CARD_FROM - i * CARD_GAP) / CARD_SPAN);
            cards[i].style.transform = 'translateY(' + (16 * (1 - u)).toFixed(3) + 'cqw)';
            cards[i].style.opacity = u.toFixed(3);
        }
        const txt = sl.mode === M_DONE ? 'Unlocked' : 'Slide to unlock';
        if (main.label.textContent !== txt) {
            main.label.textContent = txt;
        }
    }

When You Turn a Slide to Unlock Button Into Code, How Many States Does the Handle Need?

Five states are enough

M_IDLE (resting), M_DRAG (held), M_BACK (spring to the start), M_FIN (spring to the end) and M_DONE (finished) are the whole list. A press enters M_DRAG, a release goes to M_BACK or M_FIN by the verdict result, and when the spring stops the handle lands in M_IDLE or M_DONE.

Leave one way back after completion

A scene a person has touched returns to the start DONE_HOLD_S 1.8 s after completion, and on the keyboard Enter, Space or Home brings it back. The auto demo raises cycle by one every LOOP_S, so a hand that finishes and a hand that falls back take turns.

Where to attach it: payment, power and first screens

Take the stage colors of 06 for transfer and order confirmation, the arc of 08 for power off and emergency stop, and 09 for the first screen of an app or kiosk. A slide to unlock css file can draw the track and fill, but reading release speed and spring return takes script, so the two values worth exposing to settings are the threshold ratio and the spring strength.

Measure the Track, Turn the Handle Position Into Progress, Decide Completion From the Release Point and Speed, Then Spring Back When It Falls Short: the Order in Which One Slide to Unlock Gets Handled

The track is measured in cqw so it scales with the stage

Handle travel is TRACK_CQW − HANDLE_CQW − 2·PAD_CQW, which is 57 cqw for 72 and 273.6 px on a 480 px stage. The unit is cqw, not px, so a narrower stage shrinks everything by the same ratio.

The handle trails the finger and the speed is smoothed

While held, position follows p += (mapDrag(x) − p)·(1 − e^(−h·FOLLOW)) and speed follows vEma += (dp/h − vEma)·(1 − e^(−h·VEL_SMOOTH)), with FOLLOW at 40 and VEL_SMOOTH at 20. Because the handle trails the finger by a beat, the speed read at release does not jitter from frame to frame.

One frame is cut into 1/240 s slices

update splits the frame into ceil(dt·240) slices and runs stepPhysics on each, and one frame is allowed at most DT_MAX 0.05 s. A different frame rate therefore draws the same spring curve.

    function update(dt) {
        const n = Math.max(1, Math.ceil(dt * 240));
        const h = dt / n;
        for (let i = 0; i < n; i += 1) {
            stepPhysics(h);
            tick(h);
        }
    }
    // upper limit on time per frame (seconds): even the first frame after a long hide counts only up to 50 ms
    const DT_MAX = 0.05;
    function advance(dt) {
        sceneT += dt;
        if (demo && sceneT >= LOOP_S) {
            sceneT -= LOOP_S;
            cycle += 1;
            fullReset();
        }
        update(dt);
    }

Where Slide to Unlock Breaks: a Handle That Slides Past the End, a Fill That Stays Stuck Halfway After Release and a Drag That Cuts Off When the Touch Leaves the Track

No limit and the handle flies past the end

The small track under 01 follows the finger with badX = clamp(fx, 0, 1.3), so its handle flies past the end. The big track above stops at 1.078 for the same hand, which lets you compare both ways on one screen.

No return and the fill stays halfway

The contrast version in 03 simply stops where it is let go, because badP follows clamp(p, 0, 1) only while held. A release at 0.525 leaves a half full fill standing there.

The pointer leaves the track and the drag cuts off

The contrast version in 06 sets lost = true once the finger passes LEAVE_X 0.55, and the handle freezes in place and stops following the finger. The real handle calls setPointerCapture and carries touch-action: none, so the drag keeps going even when the finger leaves the track. Open the zip with the password smsvvdkv and drag the handle past 0.55 in the 06 folder's index.html to watch the small line below cut off on the spot.

Change the Track Length, the Threshold Ratio and the Spring Strength and How Much Does the Verdict Shift? Width, Threshold and Stiffness Compared

Each of the three values was changed alone and run against the same demo hand. The numbers are not render measurements but a simulation of stepPhysics at 60 Hz frames in 1/240 s slices.

Name Changed value Feel
Longer track 02 TRACK_CQW 72 → 84 Handle travel grows from 57 cqw to 69 cqw (274 px to 331 px on a 480 px stage). The same hand, releasing at the 138 px point at 369 px per second, reads as progress 0.50 and speed 1.35 on the short track but 0.42 and 1.12 on the long one, so the projected landing falls from 0.84 to 0.69, under the 0.80 threshold, and a finish flips into a return
Higher threshold 04 THRESH 0.6 → 0.85 The hand that drags to 0.75 and lets go auto finishes at 1.03 s while held at 0.6 (done state at 1.82 s), but at 0.85 it is still short of the threshold at the release at 1.62 s. It passes the start by about 5% and settles there at 2.47 s
Stiffer spring 03 SPR_K 110 → 240 Damping ratio drops from 0.43 to 0.29, so the overshoot past the start grows from 22.5% to 38.5% of the release distance (a release at 0.525 reaches −0.12 versus −0.20), and the first arrival at the start moves from 0.20 s to 0.10 s. Ringing speeds up from 1.51 Hz to 2.36 Hz, and with SPR_C still 9 the time to stop is about 1.2 s either way

What to check before changing a value

The three variants use a fixed demo hand, so each only shows where one value flips a verdict, and a real hand differs every time. A lower THRESH finishes lighter and a higher one asks for a full push, so a costly screen such as payment should lean high and a casual lock can lean low.

One Unlocked Frame Under Reduced Motion: Accessibility Through Progress Read as a Sentence and Arrow Keys That Move the Handle

Reduced motion and the unlocked frame

Under prefers-reduced-motion: reduce, reduceQuery reads the media query and stops the loop, and showRest() draws one unlocked frame with p = 1 and M_DONE and no tweens. The handle can still be dragged, so pointermove changes p directly, and a release is judged at speed zero, which means the speed projection of 02 does not apply there. Letting go calls settleNow() to pick the final state at once, and a change listener reads the setting again if it flips while the page is open.

Slider role and progress read as a sentence

The handle has the slider role with aria-valuemin, aria-valuemax (0 to 100) and aria-valuenow, and aria-valuetext carries a sentence for that moment. The visible status line is aria-hidden, and completion writes one sentence from finalText() into .vj__live once. During the auto demo it is aria-live="off", and the first human touch turns it to polite.

Keyboard and pause

Right and up arrows raise progress by KEY_STEP 0.1 and left and down arrows lower it, Home goes to the start, and End, Enter and Space send it to the end. Crossing the threshold with arrows completes it as usual, and after completion Enter, Space or Home returns it to the start. The pause button reports its state with aria-pressed, and the loop stops when the tab is hidden (visibilitychange).

Slide to Unlock FAQ: Building Without a Library, Taking Touch and Mouse Together and a Confirm Button to Use Instead of Swipe to Unlock

Can slide to unlock javascript work without a library?

Yes, none of the nine scenes uses a library, WebGL or a CDN, and none calls Math.random, Date or fetch. Read the one handle with pointerdown, pointermove and pointerup, and let requestAnimationFrame draw the position.

How do I take touch and mouse together?

Pointer events receive mouse, touch and pen through one set of handlers. Call setPointerCapture in pointerdown, ignore the mouse when e.button !== 0, and treat pointercancel like a release.

What does a confirm button look like if I drop the swipe?

A single <button> can take the same confirmation without the slider. Enter and Space in these sliders already call completeNow() and jump to the end, so a keyboard user is using a button anyway, and if the goal is only to avoid mistakes, asking for a second press does the same job.

Enter the archive password

The password is inside this article. You will find it as you read.