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

v1.0.0

Published

A lightweight vanilla TypeScript library providing UI utilities for dialogs, animations, focus management, menu navigation, click outside detection, floating element positioning, and common helper functions. Framework-agnostic and designed to integrate se

Readme

Inertia UI Vanilla

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

This package is part of the Inertia UI suite. Check out our other packages:

  • Inertia Modal: Turn any Laravel route into a modal or slideover with a single component. No backend changes needed, with support for nested/stacked modals and inter-modal communication. Works with Vue and React.
  • Inertia Table: The most complete data table package for Laravel and Inertia.js. Sorting, searching, and filtering across relationships, bulk actions, CSV/Excel/PDF exports, sticky headers, and much more. Works with Vue and React.

Inertia UI

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(dialogElement)

// 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, {
    initialFocusElement: submitButton, // Focus submit button instead of first element
})

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 (<script setup>)
const cleanup = onEscapeKey(closeDialog)
onUnmounted(() => cleanup())

// React
useEffect(() => {
    return onEscapeKey(closeDialog)
}, [])

Click Outside

The onClickOutside function detects clicks outside one or more elements and calls a callback. Useful for closing dropdowns, popovers, and modals when the user clicks elsewhere.

Basic Usage

import { onClickOutside } from '@inertiaui/vanilla'

const cleanup = onClickOutside(dropdownElement, (event) => {
    closeDropdown()
})

// Later, remove the listener
cleanup()

Multiple Elements

Pass an array of elements to ignore clicks inside any of them:

const cleanup = onClickOutside([triggerButton, dropdownPanel], () => {
    closeDropdown()
})

Portal Support

Clicks inside elements with the data-inertiaui-portal attribute (or their descendants) are automatically ignored. This prevents portalled content like dropdown menus from being considered "outside":

<div data-inertiaui-portal>
    <!-- Clicks here won't trigger the callback -->
</div>

Same-Tick Protection

The listener registration is deferred by one tick, so the click that triggered the element to open won't immediately close it:

openButton.addEventListener('click', () => {
    dropdown.hidden = false
    // The click on openButton won't trigger the outside handler
    onClickOutside(dropdown, () => { dropdown.hidden = true })
})

Menu Navigation

The createMenuNavigation function adds keyboard navigation to menu containers, implementing the WAI-ARIA Menu Pattern. It supports arrow key navigation, roving tabindex, type-ahead search, and item activation.

Basic Usage

import { createMenuNavigation } from '@inertiaui/vanilla'

const cleanup = createMenuNavigation(menuElement)

// Later, remove navigation
cleanup()

Options

| Option | Type | Default | Description | |--------|------|---------|-------------| | itemSelector | string | '[role="menuitem"]:not([disabled]):not([aria-disabled="true"])' | CSS selector for menu items | | orientation | 'vertical' \| 'horizontal' | 'vertical' | Arrow key direction | | loop | boolean | true | Wrap focus from last to first item | | typeAhead | boolean | true | Enable type-ahead character search | | onActivate | (item: HTMLElement) => void | undefined | Called when an item is activated via Enter or Space |

Keyboard Support

| Key | Action | |-----|--------| | ArrowDown / ArrowRight | Focus next item (depending on orientation) | | ArrowUp / ArrowLeft | Focus previous item (depending on orientation) | | Home | Focus first item | | End | Focus last item | | Enter / Space | Activate (click) the focused item | | Any character | Type-ahead: focus the first item whose text starts with the typed characters |

Roving Tabindex

The focused item receives tabindex="0" while all other items receive tabindex="-1". This allows the menu to participate in the page's tab order with a single tab stop:

const cleanup = createMenuNavigation(menuElement)

// First item has tabindex="0", rest have tabindex="-1"
// Arrow keys move focus and update tabindex accordingly

Horizontal Menus

For horizontal menus like toolbars, use orientation: 'horizontal':

const cleanup = createMenuNavigation(toolbar, {
    orientation: 'horizontal',
})
// ArrowRight/ArrowLeft navigate instead of ArrowDown/ArrowUp

Custom Item Selector

Use a custom selector for non-standard menu structures:

const cleanup = createMenuNavigation(container, {
    itemSelector: '.menu-item:not(.disabled)',
})

Full Example

import { createMenuNavigation, onClickOutside, onEscapeKey } from '@inertiaui/vanilla'

function openMenu(menuElement: HTMLElement) {
    menuElement.hidden = false

    const cleanups = [
        createMenuNavigation(menuElement, {
            onActivate: (item) => {
                handleMenuAction(item.dataset.action!)
                closeMenu()
            },
        }),
        onClickOutside(menuElement, closeMenu),
        onEscapeKey(closeMenu),
    ]

    function closeMenu() {
        cleanups.forEach((fn) => fn())
        menuElement.hidden = true
    }
}

Positioning

Utilities for positioning floating elements (dropdowns, tooltips, popovers) relative to a reference element. Uses CSS Anchor Positioning when supported, with an automatic JavaScript fallback.

supportsAnchorPositioning

Check if the browser supports CSS Anchor Positioning:

import { supportsAnchorPositioning } from '@inertiaui/vanilla'

if (supportsAnchorPositioning()) {
    // Browser handles positioning via CSS
} else {
    // JavaScript fallback is used
}

The result is cached after the first call.

computePosition

Position a floating element relative to a reference element:

import { computePosition } from '@inertiaui/vanilla'

const result = computePosition(referenceElement, floatingElement)
// { x: 100, y: 140, placement: 'bottom-start' }

The function applies positioning styles to the floating element automatically (position: fixed with top and left values).

Options

| Option | Type | Default | Description | |--------|------|---------|-------------| | placement | Placement | 'bottom-start' | Where to position the floating element | | offset | number | 0 | Distance in pixels between reference and floating element | | flip | boolean | true | Flip to opposite side when overflowing viewport |

Placements

Twelve placement options are available, combining a side with an optional alignment:

| Side | Center | Start | End | |------|--------|-------|-----| | top | top | top-start | top-end | | bottom | bottom | bottom-start | bottom-end | | left | left | left-start | left-end | | right | right | right-start | right-end |

The start and end alignments are RTL-aware for top and bottom placements.

Flip Behavior

When the floating element would overflow the viewport, it automatically flips to the opposite side:

// If there's no room below, flips to top
const result = computePosition(reference, floating, {
    placement: 'bottom-start',
})
// result.placement may be 'top-start' if it flipped

Disable flipping to keep the element on the specified side:

const result = computePosition(reference, floating, {
    placement: 'bottom-start',
    flip: false,
})

Viewport Clamping

The floating element is always clamped to stay within the viewport with a 4px margin, even after flipping.

autoUpdate

Automatically reposition the floating element when the layout changes:

import { computePosition, autoUpdate } from '@inertiaui/vanilla'

const cleanup = autoUpdate(referenceElement, floatingElement, () => {
    computePosition(referenceElement, floatingElement, {
        placement: 'bottom-start',
        offset: 8,
    })
})

// Later, stop updating
cleanup()

autoUpdate listens for:

  • Window resize events
  • Window scroll events (including nested scrollable containers)
  • Size changes on both the reference and floating elements (via ResizeObserver)

Updates are batched using requestAnimationFrame to avoid layout thrashing.

CSS Anchor Positioning

When the browser supports CSS Anchor Positioning, computePosition uses native CSS for positioning. This provides better performance and handles edge cases like scrolling and resizing without JavaScript recalculation. The autoUpdate cleanup function automatically removes CSS anchor styles when called.

Full Example

import { computePosition, autoUpdate } from '@inertiaui/vanilla'

function setupTooltip(trigger: HTMLElement, tooltip: HTMLElement) {
    tooltip.style.display = 'block'

    const cleanup = autoUpdate(trigger, tooltip, () => {
        computePosition(trigger, tooltip, {
            placement: 'top',
            offset: 8,
        })
    })

    // Initial position
    computePosition(trigger, tooltip, {
        placement: 'top',
        offset: 8,
    })

    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 cleanup1 = markAriaHidden('#app')

// Using element
const element = document.getElementById('app')!
const cleanup2 = 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)
element.getAttribute('aria-hidden') // 'true'

cleanup()
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)
element.getAttribute('aria-hidden') // 'true'

cleanup()
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 closeDialog = () => cleanups.forEach(fn => fn())

    const cleanups = [
        markAriaHidden('#app'),
        lockScroll(),
        createFocusTrap(dialogElement),
        onEscapeKey(closeDialog),
    ]

    return closeDialog
}

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
}

Dark Mode Detection

The prefersDarkMode function detects whether the user prefers dark mode, with support for multiple detection strategies.

Basic Usage

import { prefersDarkMode } from '@inertiaui/vanilla'

const isDark = prefersDarkMode()

Strategies

| Strategy | Behavior | |----------|----------| | 'auto' (default) | Checks <html class="dark"> first, then prefers-color-scheme: dark media query | | 'class' / 'selector' | Checks <html class="dark"> | | 'media' | Checks prefers-color-scheme: dark media query | | Custom function | Called directly for full control |

// Class-based detection (e.g., Tailwind dark mode)
prefersDarkMode('class')

// Media query only
prefersDarkMode('media')

// Custom logic
prefersDarkMode(() => document.body.dataset.theme === 'dark')

RTL Support

Utilities for detecting and observing right-to-left document direction.

isRtl

Check whether the document is currently in RTL direction:

import { isRtl } from '@inertiaui/vanilla'

if (isRtl()) {
    // Document is right-to-left
}

onRtlChange

Observe changes to the document's dir attribute and invoke a callback whenever it changes. Returns a cleanup function.

import { onRtlChange } from '@inertiaui/vanilla'

const cleanup = onRtlChange((rtl) => {
    console.log('RTL changed:', rtl)
})

// Later, stop observing
cleanup()

Debounce

debounce

Debounce a function using requestAnimationFrame. Ensures the function runs at most once per animation frame.

import { debounce } from '@inertiaui/vanilla'

const handleScroll = debounce(() => {
    updatePosition()
})

window.addEventListener('scroll', handleScroll)

detectFramerate

Detect the browser's current framerate. Returns a Promise that resolves with the detected FPS (capped to the 30–240 range). Falls back to 60 if requestAnimationFrame is unavailable or detection times out.

import { detectFramerate } from '@inertiaui/vanilla'

const fps = await detectFramerate()
console.log(`Running at ${fps} FPS`)

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

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

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

blank

A port of Laravel's blank function. Returns true if the value is "empty" — null, undefined, empty string (or whitespace-only), empty array, or empty object.

import { blank } from '@inertiaui/vanilla'

blank(null)        // true
blank(undefined)   // true
blank('')          // true
blank('  ')        // true
blank([])          // true
blank('hello')     // false
blank(0)           // false
blank(false)       // false

onceChildrenRendered

Invokes a callback once the given element has child elements. If the element already has children, the callback fires immediately. Otherwise, it uses a MutationObserver to wait for children to appear.

import { onceChildrenRendered } from '@inertiaui/vanilla'

onceChildrenRendered(containerElement, () => {
    // Children are now present in the DOM
    initializeContent()
})

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: Record<string, Function> = {
    onClick: handleClick,
    onMouseOver: handleHover,
    onModalReady: handleModalReady,
    onUserUpdated: handleUserUpdated,
}

const domEvents: Record<string, Function> = {}
const customEvents: Record<string, Function> = {}

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)

Development

Running the dev server

Start Vite to browse the interactive test pages:

npx vite --port 3333

Then open http://localhost:3333 for an overview linking to all test pages.

Running E2E tests

The test suite uses Playwright with Chromium. It automatically starts a Vite dev server:

npm run test:e2e

Run a single spec:

npx playwright test e2e/menu.spec.ts

Run tests matching a name:

npx playwright test -g "ArrowDown"

Test structure

Each feature has a spec file and a matching HTML page:

e2e/click-outside.spec.ts  →  e2e/pages/click-outside.html
e2e/menu.spec.ts           →  e2e/pages/menu.html
e2e/focus-trap.spec.ts     →  e2e/pages/focus-trap.html
...

The mapping is handled by a custom fixture in e2e/test.ts. HTML pages import directly from the TypeScript source via Vite.

TypeScript

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

import type {
    CleanupFunction,
    FocusTrapOptions,
    EscapeKeyOptions,
    AnimateOptions,
    EasingName,
    MenuNavigationOptions,
    Placement,
    PositionOptions,
    PositionResult,
    DarkModeStrategy,
} from '@inertiaui/vanilla'

License

MIT