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

@inertiaui/vanilla

v0.2.0

Published

A lightweight vanilla TypeScript library providing UI utilities for dialogs, animations, focus management, and common helper functions. Framework-agnostic and designed to integrate seamlessly with Vue, React, or any JavaScript application.

Readme

Inertia UI Vanilla

A lightweight vanilla TypeScript library providing UI utilities for dialogs, animations, focus management, and common helper functions. Framework-agnostic and designed to integrate seamlessly with Vue, React, or any JavaScript application.

Installation

npm install @inertiaui/vanilla

Table of Contents

Scroll Locking

The lockScroll function prevents body scroll while dialogs or modals are open, with reference counting support for nested dialogs.

Basic Usage

import { lockScroll } from '@inertiaui/vanilla'

const unlock = lockScroll()

// Later, unlock
unlock()

The function:

  • Sets document.body.style.overflow to 'hidden'
  • Adds padding to compensate for scrollbar width (prevents layout shift)
  • Returns a cleanup function that can only unlock once

Reference Counting

Multiple calls to lockScroll are reference counted. The body scroll is only restored when all locks are released:

import { lockScroll } from '@inertiaui/vanilla'

const unlock1 = lockScroll()
const unlock2 = lockScroll()

// Body is locked

unlock1()
// Body is still locked (one reference remaining)

unlock2()
// Body scroll is restored

Idempotent Unlock

Each cleanup function can only unlock once, preventing accidental double-unlocking:

const unlock = lockScroll()
unlock() // Decrements count
unlock() // No effect
unlock() // No effect

Focus Management

Focus management utilities help create accessible dialogs by trapping focus and managing focusable elements.

createFocusTrap

Creates a focus trap within a container element.

import { createFocusTrap } from '@inertiaui/vanilla'

const cleanup = createFocusTrap(container, {
    initialFocus: true,
    initialFocusElement: null,
    returnFocus: true,
})

// Later, remove the focus trap
cleanup()

Options

| Option | Type | Default | Description | |--------|------|---------|-------------| | initialFocus | boolean | true | Focus first element immediately | | initialFocusElement | HTMLElement \| null | null | Specific element to focus initially | | returnFocus | boolean | true | Return focus to previous element on cleanup |

Behavior

The focus trap:

  • Listens for Tab key and wraps focus at container boundaries
  • Prevents focus from leaving the container via Tab or Shift+Tab
  • Catches focus that escapes (e.g., via mouse click outside)
  • Optionally focuses the first focusable element on creation
  • Optionally returns focus to the previously focused element on cleanup
  • Supports nesting: when multiple traps are active, only the most recently created trap receives focus. Cleaning up the inner trap restores the outer trap.
const container = document.getElementById('dialog')
const submitButton = document.getElementById('submit')

const cleanup = createFocusTrap(container, {
    initialFocus: true,
    initialFocusElement: submitButton, // Focus submit button instead of first element
    returnFocus: true,
})

Focusable Elements

The focus trap recognizes these elements as focusable:

  • a[href]
  • button:not([disabled])
  • textarea:not([disabled])
  • input:not([disabled])
  • select:not([disabled])
  • [tabindex]:not([tabindex="-1"])

Elements with aria-hidden="true" are excluded. (Elements with disabled are already filtered by the selectors above.)

Keyboard Events

onEscapeKey

Registers an Escape key handler.

import { onEscapeKey } from '@inertiaui/vanilla'

const cleanup = onEscapeKey((event) => {
    console.log('Escape pressed!')
})

// Later, remove the handler
cleanup()

Options

| Option | Type | Default | Description | |--------|------|---------|-------------| | preventDefault | boolean | false | Call event.preventDefault() | | stopPropagation | boolean | false | Call event.stopPropagation() |

const cleanup = onEscapeKey(handleEscape, {
    preventDefault: true,
    stopPropagation: true,
})

Cleanup Pattern

The cleanup function pattern integrates well with framework lifecycle hooks:

// Vue
onMounted(() => {
    cleanup = onEscapeKey(close)
})
onUnmounted(() => {
    cleanup()
})

// React
useEffect(() => {
    const cleanup = onEscapeKey(close)
    return cleanup
}, [])

Accessibility

Accessibility utilities for managing aria-hidden attributes with reference counting support.

markAriaHidden

Marks an element as aria-hidden="true" and returns a cleanup function.

import { markAriaHidden } from '@inertiaui/vanilla'

const cleanup = markAriaHidden('#app')

// Later, restore
cleanup()

Accepts either an element or a CSS selector:

// Using selector
const cleanup = markAriaHidden('#app')

// Using element
const element = document.getElementById('app')
const cleanup = markAriaHidden(element)

Reference Counting

Like scroll locking, aria-hidden management uses reference counting for nested dialogs:

import { markAriaHidden } from '@inertiaui/vanilla'

const cleanup1 = markAriaHidden('#app')
const cleanup2 = markAriaHidden('#app')

// Element is aria-hidden="true"

cleanup1()
// Element is still aria-hidden="true" (one reference remaining)

cleanup2()
// Element's aria-hidden is restored to original value

Original Value Preservation

The original aria-hidden value is preserved and restored:

const element = document.getElementById('sidebar')
element.setAttribute('aria-hidden', 'false')

const cleanup = markAriaHidden(element)
console.log(element.getAttribute('aria-hidden')) // 'true'

cleanup()
console.log(element.getAttribute('aria-hidden')) // 'false' (restored)

If the element had no aria-hidden attribute, the attribute is removed on cleanup:

const element = document.getElementById('main')
// No aria-hidden attribute

const cleanup = markAriaHidden(element)
console.log(element.getAttribute('aria-hidden')) // 'true'

cleanup()
console.log(element.getAttribute('aria-hidden')) // null (removed)

Use with Dialogs

When a dialog opens, the main content should be marked as aria-hidden to prevent screen readers from reading background content:

import { markAriaHidden, lockScroll, createFocusTrap, onEscapeKey } from '@inertiaui/vanilla'

function openDialog(dialogElement: HTMLElement) {
    const cleanups = [
        markAriaHidden('#app'),
        lockScroll(),
        createFocusTrap(dialogElement),
        onEscapeKey(() => closeDialog()),
    ]

    return () => cleanups.forEach(cleanup => cleanup())
}

Animation

The animation module provides a simple wrapper around the Web Animations API with Tailwind CSS-compatible easing functions.

animate

Animate an element using the Web Animations API. Returns a promise that resolves when the animation completes. If the animation is cancelled (e.g., by calling cancelAnimations), the promise resolves with the Animation object instead of rejecting.

import { animate } from '@inertiaui/vanilla'

await animate(element, [
    { transform: 'scale(0.95)', opacity: 0 },
    { transform: 'scale(1)', opacity: 1 }
])

Options

| Option | Type | Default | Description | |--------|------|---------|-------------| | duration | number | 300 | Animation duration in milliseconds | | easing | string \| EasingName | 'inOut' | Easing function (see below) | | fill | FillMode | 'forwards' | Animation fill mode |

await animate(element, keyframes, { duration: 200, easing: 'out' })

easings

Pre-defined easing functions matching Tailwind CSS:

import { easings } from '@inertiaui/vanilla'

// Available easings:
easings.linear  // 'linear'
easings.in      // 'cubic-bezier(0.4, 0, 1, 1)'
easings.out     // 'cubic-bezier(0, 0, 0.2, 1)'
easings.inOut   // 'cubic-bezier(0.4, 0, 0.2, 1)'

You can use easing names directly:

await animate(element, keyframes, { easing: 'out' })

Or provide a custom easing string:

await animate(element, keyframes, { easing: 'cubic-bezier(0.68, -0.55, 0.27, 1.55)' })

cancelAnimations

Cancel any running animations on an element:

import { cancelAnimations } from '@inertiaui/vanilla'

cancelAnimations(element)

Full Example

import { animate, cancelAnimations } from '@inertiaui/vanilla'

async function showModal(modal: HTMLElement) {
    modal.hidden = false

    await animate(modal, [
        { transform: 'scale(0.95)', opacity: 0 },
        { transform: 'scale(1)', opacity: 1 }
    ], { duration: 150, easing: 'out' })
}

async function hideModal(modal: HTMLElement) {
    await animate(modal, [
        { transform: 'scale(1)', opacity: 1 },
        { transform: 'scale(0.95)', opacity: 0 }
    ], { duration: 100, easing: 'in' })

    modal.hidden = true
}

function forceHideModal(modal: HTMLElement) {
    cancelAnimations(modal)
    modal.hidden = true
}

Helpers

generateId

Generates a unique ID using crypto.randomUUID() with a fallback for environments where it's not available.

import { generateId } from '@inertiaui/vanilla'

const id = generateId()
// 'inertiaui_550e8400-e29b-41d4-a716-446655440000'

Custom Prefix

const id = generateId('modal_')
// 'modal_550e8400-e29b-41d4-a716-446655440000'

const id = generateId('dialog-')
// 'dialog-550e8400-e29b-41d4-a716-446655440000'

Fallback

In environments where crypto.randomUUID() is not available, the function falls back to a combination of timestamp and random string:

// Fallback format:
// '{prefix}{timestamp}_{random}'
// 'inertiaui_m5x2k9p_7h3j5k9a2'

Use Cases

Useful for generating unique IDs for:

  • Dialog instances
  • Form elements requiring unique IDs
  • Accessibility attributes (aria-labelledby, aria-describedby)
  • Tracking modal instances
const dialogId = generateId('dialog_')
const titleId = generateId('title_')
const descId = generateId('desc_')

dialog.setAttribute('aria-labelledby', titleId)
dialog.setAttribute('aria-describedby', descId)
title.id = titleId
description.id = descId

Object Filtering

except

Returns an object or array without the specified keys/elements.

Objects:

import { except } from '@inertiaui/vanilla'

const obj = { a: 1, b: 2, c: 3 }
except(obj, ['b'])
// { a: 1, c: 3 }

Arrays:

const arr = ['a', 'b', 'c', 'd']
except(arr, ['b', 'd'])
// ['a', 'c']

Case-Insensitive Matching:

const obj = { Name: 1, AGE: 2, city: 3 }
except(obj, ['name', 'age'], true)
// { city: 3 }

const arr = ['Name', 'AGE', 'city']
except(arr, ['name', 'age'], true)
// ['city']

only

Returns an object or array with only the specified keys/elements.

Objects:

import { only } from '@inertiaui/vanilla'

const obj = { a: 1, b: 2, c: 3 }
only(obj, ['a', 'c'])
// { a: 1, c: 3 }

Arrays:

const arr = ['a', 'b', 'c', 'd']
only(arr, ['b', 'd'])
// ['b', 'd']

Case-Insensitive Matching:

const obj = { Name: 1, AGE: 2, city: 3 }
only(obj, ['name', 'city'], true)
// { Name: 1, city: 3 }

rejectNullValues

Removes null values from an object or array.

Objects:

import { rejectNullValues } from '@inertiaui/vanilla'

const obj = { a: 1, b: null, c: 3 }
rejectNullValues(obj)
// { a: 1, c: 3 }

Arrays:

const arr = [1, null, 3, null, 5]
rejectNullValues(arr)
// [1, 3, 5]

Note: rejectNullValues only removes null values, not undefined. Use this when you want to keep undefined values but remove explicit nulls.

String Utilities

kebabCase

Converts a string to kebab-case.

import { kebabCase } from '@inertiaui/vanilla'

kebabCase('camelCase')       // 'camel-case'
kebabCase('PascalCase')      // 'pascal-case'
kebabCase('snake_case')      // 'snake-case'
kebabCase('already-kebab')   // 'already-kebab'

Handling Special Cases:

kebabCase('user123Name')           // 'user123-name'
kebabCase('multiple__underscores') // 'multiple-underscores'
kebabCase('UPPERCASE')             // 'uppercase'
kebabCase('XMLDocument')           // 'xml-document'
kebabCase('hello world')           // 'hello-world'

isStandardDomEvent

Checks if an event name is a standard DOM event.

import { isStandardDomEvent } from '@inertiaui/vanilla'

isStandardDomEvent('onClick')     // true
isStandardDomEvent('onMouseOver') // true
isStandardDomEvent('onKeyDown')   // true
isStandardDomEvent('onCustom')    // false

Supported Event Categories:

  • Mouse events: click, dblclick, mousedown, mouseup, mouseover, mouseout, mousemove, mouseenter, mouseleave
  • Keyboard events: keydown, keyup, keypress
  • Form events: focus, blur, change, input, submit, reset
  • Window events: load, unload, error, resize, scroll
  • Touch events: touchstart, touchend, touchmove, touchcancel
  • Pointer events: pointerdown, pointerup, pointermove, pointerenter, pointerleave, pointercancel
  • Drag events: drag, dragstart, dragend, dragenter, dragleave, dragover, drop
  • Animation events: animationstart, animationend, animationiteration
  • Transition events: transitionstart, transitionend, transitionrun, transitioncancel

Case Insensitive:

isStandardDomEvent('onclick')  // true
isStandardDomEvent('ONCLICK')  // true
isStandardDomEvent('OnClick')  // true

Use Case:

Useful for distinguishing between standard DOM events and custom events when processing event handlers:

const props = {
    onClick: handleClick,
    onMouseOver: handleHover,
    onModalReady: handleModalReady,
    onUserUpdated: handleUserUpdated,
}

const domEvents = {}
const customEvents = {}

for (const [key, value] of Object.entries(props)) {
    if (isStandardDomEvent(key)) {
        domEvents[key] = value
    } else {
        customEvents[key] = value
    }
}

// domEvents: { onClick, onMouseOver }
// customEvents: { onModalReady, onUserUpdated }

URL Utilities

sameUrlPath

Compares two URLs to determine if they have the same origin and pathname, ignoring query strings and hash fragments.

import { sameUrlPath } from '@inertiaui/vanilla'

sameUrlPath('/users/1', '/users/1')           // true
sameUrlPath('/users/1', '/users/1?tab=posts') // true
sameUrlPath('/users/1', '/users/2')           // false
sameUrlPath('/users', '/posts')               // false

Accepts URL objects:

const url1 = new URL('https://example.com/users/1')
const url2 = new URL('https://example.com/users/1?page=2')

sameUrlPath(url1, url2) // true

Handles null/undefined:

sameUrlPath(null, '/users')      // false
sameUrlPath('/users', undefined) // false
sameUrlPath(null, null)          // false

Use Case:

Useful for determining active navigation states or comparing the current route with a link destination:

const isActive = sameUrlPath(window.location.href, linkHref)

TypeScript

This library is written in TypeScript and exports the following types:

import type {
    CleanupFunction,
    FocusTrapOptions,
    EscapeKeyOptions,
    AnimateOptions,
    EasingName,
} from '@inertiaui/vanilla'

License

MIT