second-finger
v0.1.0
Published
When users rage-click a button, the cursor switches to the backup finger.
Downloads
159
Maintainers
Readme
second-finger
When users rage-click a button, the cursor switches to the backup finger.
Index finger's tired. Switching to backup…
Try the live demo → · View on npm · Source on GitHub

A tiny opt-in easter egg for broken buttons, slow submits, and loading spinners that never load. Rage-click 5 times within 0.8s (configurable) and the backup finger clocks in for a few seconds.
Install
npm install second-fingerQuick start
import { secondFinger } from "second-finger";
secondFinger("#submit");Integration
1. HTML attribute (zero JS logic)
<button data-second-finger>Submit</button>
<script type="module">
import "second-finger/auto";
</script>2. One-liner (vanilla JS)
import { secondFinger } from "second-finger";
secondFinger("#submit");Returns a cleanup function:
const stop = secondFinger(document.querySelector(".pay-btn")!);
stop();3. React wrapper
"use client";
import { SecondFinger } from "second-finger/react";
export function PayButton() {
return (
<SecondFinger>
<button type="button">Pay now</button>
</SecondFinger>
);
}SecondFinger requires exactly one element child and forwards refs.
Pricing grid (mode: "always")
Swap the cursor on one tier only. No rage clicks required.
secondFinger("#pricing-free-tier", { mode: "always" });
Next.js App Router
Mark the component file with "use client" and wrap the button. No Provider, no app shell changes.
Options
Both integrations accept optional overrides:
secondFinger("#submit", {
mode: "rage",
threshold: 7,
windowMs: 1200,
message: "First finger is on break. Backup deployed.",
durationMs: 8000,
popover: "cursor",
onActivate: () => console.log("backup engaged"),
onRestore: () => console.log("cursor restored"),
});
// Or swap the cursor on one element immediately, no clicks required
secondFinger("#pricing-free-tier", { mode: "always" });| Option | Default | Description |
| ------ | ------- | ----------- |
| mode | "rage" | "rage" activates after rapid clicks; "always" swaps the cursor on the attached target only |
| threshold | 5 | Clicks within the rage window before activation (mode: "rage" only) |
| windowMs | 800 | Sliding window (ms). All threshold clicks must land within this span. Pause longer and the counter resets. |
| message | "Index finger's tired. Switching to backup…" | Popover text shown on activation |
| durationMs | 5000 | How long the backup cursor and popover stay active |
| popover | "cursor" | "cursor" follows the pointer. "bottom" is reserved for a fixed bottom pill but is not implemented yet — it currently behaves like "cursor". |
| onActivate | (none) | Called once when the backup finger activates (or immediately for mode: "always") |
| onRestore | (none) | Called when the cursor and popover are restored (or when mode: "always" is detached) |

How fast do you have to click?
Only applies to mode: "rage". By default, 5 clicks within 800ms, roughly one click every 160ms. That's genuine rage-click speed, not a leisurely double-tap. The tracker uses a sliding window: if you pause for more than windowMs, earlier clicks drop off and you start over. Import RAGE_WINDOW_MS from the package if you want the constant in your app.
CDN
<button id="submit">Submit</button>
<script type="module">
import { secondFinger } from "https://esm.sh/second-finger";
secondFinger("#submit");
</script>Behavior
mode: "rage" (default)
- Counts rapid clicks on the attached element only. Default 5 clicks within 800ms
- Shows a cursor-following popover on activation, but only while the backup finger cursor is active over a pointer target (buttons, links, etc.)
- On desktop: swaps only
pointer(hand) cursors to the backup finger for the configured duration. Default arrow stays normal - Extra rage clicks during an active session extend the duration
- On touch or coarse-pointer devices: no popover and no fake cursor (
onActivate/onRestorestill run) - With
prefers-reduced-motion: reduceandpopover: "bottom", skips the fake cursor entirely
mode: "always"
- Swaps the cursor to the backup finger on the attached element and its children immediately
- Other elements on the page keep their normal cursor. Useful for pricing grids or single CTAs
- No click counting, popover, or auto-timeout
Notes
- Does not call
preventDefault. Your button keeps working
Disclaimer
Opt-in easter egg. Some corporate networks or filters may block the imagery. Don't attach to critical production flows unless that's intentional.
License
- Code: MIT
- Cursor graphic: Public Domain, Middle Finger by bunruru (see ATTRIBUTION.md)
Demo (local)
git clone https://github.com/ahmdhsn-dev/second-finger.git
cd second-finger
npm install
npm run devOpen the local demo, tweak the config panel, and click any example button until the backup finger shows up. Same options as the live demo.
