npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@wrium/evasive-button

v1.0.0

Published

A fun evasive button plugin for Wrium - causes buttons to playfully dodge the cursor when clicked or hovered.

Readme

@wrium/evasive-button

A fun, highly practical plugin for Wrium that makes buttons (or any interactive element) playfully and smoothly dodge the user's cursor when approached, hovered, or clicked.

Ideal for gamified forms, smart validation ("complete fields to unlock button"), interactive confirmations, or playful login screens!


Highlights

  • 🏃 Smooth Tangential Gliding: Physics-based wall-sliding that avoids erratic jumps, teleportation, or sticking against boundaries.
  • 📐 Boundary & Arena Control: Confine movement to 'parent', 'viewport', or any custom CSS selector (bounds: '#arena').
  • 🎯 Movement Limits:
    • maxRadius: Restrict evasion distance from the button's home anchor so it stays in its neighborhood.
    • duration: Auto-timeout (in milliseconds) after which the button gives up and settles.
    • maxAttempts: Auto-settle after a maximum number of dodge attempts.
  • 🔄 Form Validation / Reactive Toggles: Bind directly to expressions like v-evade="{ active: !isFormValid }". When valid, the button smoothly glides home to (0, 0)!
  • 🎨 State Classes & Attributes: Automatically manages .is-settled, .is-evading, and data-state="settled|evading|gaveup" for styling.
  • 🔔 Rich Lifecycle Events: @evade, @settle, and @giveup DOM events.
  • ♿ Accessible by Design: Respects prefers-reduced-motion and preserves keyboard navigation (Tab + Enter/Space).
  • ⚡ Minimalist: Zero dependencies, GPU-accelerated transforms, ~1.5 KB.

Installation

npm install @wrium/evasive-button
# or
bun add @wrium/evasive-button

@wrium/wrium is a peer dependency — install it too if you haven't already.


Quick Start

Basic Usage

<div id="app">
    <button v-evade>Try to Click Me!</button>
</div>

<script type="module">
    import { createApp } from '@wrium/wrium';
    import { EvasiveButtonPlugin } from '@wrium/evasive-button';

    createApp(() => ({}))
        .use(EvasiveButtonPlugin)
        .mount('#app');
</script>

Practical Form Validation Scenario

The button evades the cursor while required fields are incomplete; as soon as the form is valid, it stops evading and smoothly returns to its default position, ready to be clicked!

<div id="app" class="login-card">
    <h2>Secure Login</h2>

    <input type="text" v-model="username" placeholder="Username (3+ chars)" />
    <input type="password" v-model="password" placeholder="Password (6+ chars)" />

    <!-- Evasion container arena -->
    <div id="button-arena" class="arena">
        <button
            type="submit"
            class="login-btn"
            v-evade="{
                active: !isFormValid,
                bounds: '#button-arena',
                maxRadius: 150,
                threshold: 75,
                duration: 10000
            }"
            @settle="onButtonSettled"
            @evade="onButtonEvaded"
        >
            {{ isFormValid ? 'Log In' : 'Fill fields first!' }}
        </button>
    </div>
</div>

<script type="module">
    import { createApp, ref, computed } from '@wrium/wrium';
    import { EvasiveButtonPlugin } from '@wrium/evasive-button';

    createApp(() => {
        const username = ref('');
        const password = ref('');

        const isFormValid = computed(() => {
            return username.value.trim().length >= 3 && password.value.length >= 6;
        });

        function onButtonSettled(e) {
            console.log('Button has returned home and settled!', e.detail);
        }

        function onButtonEvaded(e) {
            console.log('Button evaded, total attempts:', e.detail.attempts);
        }

        return { username, password, isFormValid, onButtonSettled, onButtonEvaded };
    })
        .use(EvasiveButtonPlugin)
        .mount('#app');
</script>

Directive Options Reference

Options can be passed directly as an object to v-evade="{ ... }" or globally when installing via app.use(EvasiveButtonPlugin, { ... }):

| Option | Type | Default | Description | |--------|------|---------|-------------| | active | boolean | true | When false, evasion stops and the button smoothly returns to (0, 0) | | bounds | string | 'parent' | Bounding container: 'parent', 'viewport', or a CSS selector (e.g. '#arena') | | maxRadius | number | Infinity | Maximum radius (in pixels) the button is allowed to stray from its natural anchor | | duration | number | Infinity | Maximum time (in ms) to evade before giving up and settling | | maxAttempts | number | Infinity | Maximum number of dodges before giving up and settling | | threshold | number | 75 | Proximity trigger distance (in pixels) from the cursor | | distance | number | 100 | Base leap distance (in pixels) away from the cursor | | random | boolean | false | Adds angular jitter to evasion trajectories | | teleport | boolean | false | Jump instantly without smooth CSS transition animation | | allowKeyboard | boolean | true | Allows keyboard users (Tab + Enter/Space) to submit without triggering evasion | | respectReducedMotion | boolean | true | Automatically disables evasion if OS prefers-reduced-motion is active |


State Classes & Attributes

The directive automatically maintains CSS classes and data attributes on the element, making visual styling effortless:

  • .is-settled / data-state="settled": Present when the button is at rest at its natural origin.
  • .is-evading / data-state="evading": Present while the button is actively evading or displaced.
  • data-state="gaveup": Present when evasion stopped due to duration timeout or maxAttempts.

Styling Example:

/* Normal state */
.login-btn.is-settled {
    background: #0284c7;
}

/* While dodging the cursor */
.login-btn.is-evading {
    background: #e11d48;
    box-shadow: 0 4px 14px rgba(225, 29, 72, 0.4);
}

Lifecycle Events

Elements dispatch custom DOM events that can be listened to via @event in Wrium templates:

@evade

Fires on each evasion:

e.detail: {
    attempts: number, // Total number of evasions triggered
    x: number,        // Current translation X in px
    y: number,        // Current translation Y in px
    dx: number,       // Delta X moved this step
    dy: number,       // Delta Y moved this step
    source: string    // 'proximity' | 'enter' | 'click_attempt'
}

@settle

Fires when evasion stops and the button has smoothly landed back at (0, 0):

e.detail: {
    attempts: number,
    reason: string    // 'disabled' | 'duration' | 'maxAttempts'
}

@giveup

Fires when evasion automatically stops because duration or maxAttempts was reached:

e.detail: {
    attempts: number,
    reason: string    // 'duration' | 'maxAttempts'
}

Standalone Helper Functions

Pure mathematical functions exported for standalone use outside of DOM bindings:

import { calculateEvasionOffset, isNearTarget, isReducedMotionPreferred } from '@wrium/evasive-button';

// Check proximity
const { isNear, distance } = isNearTarget(cursor, targetRect, 80);

// Calculate next offset respecting boundaries and maxRadius
const nextOffset = calculateEvasionOffset(
    cursor,
    elSize,
    naturalPos,
    containerBounds,
    currentOffset,
    { distance: 100, threshold: 80, maxRadius: 150 }
);

Testing

Run unit tests (Vitest):

bun run test
# or: npm test

Run End-to-End tests (Playwright):

bun run test:e2e
# or: npm run test:e2e

Interactive UI Test Mode:

bun run test:e2e:ui

License

MIT License.