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

@magic-spells/panel-stack

v0.1.0

Published

A nested-panel stack web component with fluid push/pop transitions — drill-down menus, settings flows, multi-step wizards.

Readme

@magic-spells/panel-stack

A nested-panel stack web component with fluid push/pop transitions. Built for mobile menus, settings drill-downs, multi-step wizards, and anything that wants the feel of a navigation stack.

🔍 Live Demo — See it in action!

  • 1.27 KB JS gzip, 0.77 KB CSS gzip, zero dependencies
  • Two custom elements: <panel-stack> + <stack-panel>
  • Honors prefers-reduced-motion
  • inert on hidden panels — focus stays where it should
  • All motion tunable via CSS custom properties

Install

npm i @magic-spells/panel-stack
import '@magic-spells/panel-stack';
<link rel="stylesheet" href="https://unpkg.com/@magic-spells/panel-stack/css/min" />

Markup

<panel-stack initial="root">
  <stack-panel handle="root">
    <button data-action-stack-push target="shop">Shop</button>
    <a href="/about">About</a>
  </stack-panel>

  <stack-panel handle="shop">
    <button data-action-stack-pop>Back</button>
    <button data-action-stack-push target="shop-women">Women</button>
  </stack-panel>

  <stack-panel handle="shop-women">
    <button data-action-stack-pop>Back</button>
    <a href="/dresses">Dresses</a>
  </stack-panel>
</panel-stack>

The parent of <panel-stack> needs a defined size — panels are position: absolute; inset: 0 and the stack clips them.

API

const stack = document.querySelector('panel-stack');

stack.push('shop');              // slide to a panel
stack.push('shop', triggerEl);   // optional 2nd arg — pop() restores focus to it
stack.pop();                     // back one level
stack.reset();                   // collapse to root

stack.currentHandle;   // 'shop'
stack.currentPanel;    // <stack-panel handle="shop">
stack.depth;           // 2

Events

All events bubble + composed. panel-stack:push is cancelable.

stack.addEventListener('panel-stack:push', (e) => {
  console.log(e.detail); // { fromHandle: 'root', toHandle: 'shop' }
});

stack.addEventListener('panel-stack:pop', (e) => { /* { fromHandle, toHandle } */ });
stack.addEventListener('panel-stack:reset', (e) => { /* { rootHandle } */ });

// Cancel a push:
stack.addEventListener('panel-stack:push', (e) => {
  if (e.detail.toHandle === 'admin' && !user.isAdmin) e.preventDefault();
});

Effects

Two visual styles for how state="previous" panels look. Pick one with the effect attribute:

<panel-stack effect="slide"> … </panel-stack>   <!-- default: slides off to the left -->
<panel-stack effect="stack"> … </panel-stack>   <!-- shrinks + blurs + dims behind current -->

effect="stack" keeps the previous panel in place while it scales to 0.95, blurs 1px, dims to brightness(0.5), and drops to z-index: -1 behind the current panel. Pop, and it pops back to full size, sharp, and bright.

Both effects share the same per-state CSS variables, so you can fine-tune either one.

Rounded corners

<panel-stack> and <stack-panel> use border-radius: inherit. Wrap the stack in a container with a radius and the panels pick it up automatically:

<div style="border-radius: 22px; overflow: hidden; width: 320px; height: 540px;">
  <panel-stack effect="stack"> … </panel-stack>
</div>

CSS custom properties

Global timing and perspective:

| Property | Default | Description | | --- | --- | --- | | --ps-transition-duration | 420ms | Animation duration (transform + filter) | | --ps-transition-timing | cubic-bezier(0.16, 0.87, 0.64, 1) | Easing | | --ps-perspective | 1200px | Depth of the 3D scene |

Per-state position and filter values. Each state has its own translate, scale, blur, opacity, brightness, and z-index — override one state without affecting the others:

| Property | Default (current · previous · next) | | --- | --- | | --ps-translate-{state} | 0% · calc(-100% - 50px) · calc(100% + 50px) | | --ps-scale-x-{state} | 1 · 1.1 · 1.1 | | --ps-scale-y-{state} | 1 · 1 · 1 | | --ps-blur-{state} | 0px · 2px · 2px | | --ps-opacity-{state} | 1 · 0.1 · 0.1 | | --ps-z-index-{state} | 1 · 0 · 2 |

--ps-brightness-previous (default 1) is the only brightness knob — effect="stack" uses it to darken the receding panel.

--ps-scale-x-{state} and --ps-scale-y-{state} accept any number — values < 1 shrink the panel, values > 1 stretch it, negative values flip it (mirror).

effect="stack" overrides the previous defaults to: translate 0%, scale 0.95, blur 1px, opacity 1, brightness 0.5, z-index -1. It also sets --ps-opacity-next: 1 so panels coming in from the right aren't faded during the swap.

Example — make the slide flat (no scale, no blur):

panel-stack {
  --ps-scale-x-previous: 1;
  --ps-scale-x-next: 1;
  --ps-blur-previous: 0px;
  --ps-blur-next: 0px;
}

Focus

On push() and reset(), focus moves to the new current panel — specifically:

  1. The first descendant with data-stack-focus, if one exists
  2. Otherwise the first focusable child (button, link, input, etc.)

On pop(), focus is restored to the element that originally pushed the panel you're leaving — matching native back-button behavior. Declarative data-action-stack-push triggers are remembered automatically; for programmatic pushes pass the trigger as the second arg: stack.push('shop', triggerEl). If the trigger has been removed from the DOM, pop falls back to the destination panel's first focusable.

Inactive panels get inert so they can't trap tab navigation or screen reader focus.

Keyboard

Pressing Escape while focus is inside the stack pops one level — but only when there's somewhere to go back to. At the root panel, Esc bubbles untouched so a wrapping <dialog> closes as normal:

<dialog>
  <panel-stack>
    <stack-panel handle="root">…</stack-panel>
    <stack-panel handle="settings">…</stack-panel>
  </panel-stack>
</dialog>

Drill into settings, hit Esc → goes back to root, dialog stays open. Hit Esc again → dialog closes. No coordination needed between the two.

If a focused input already consumed Esc (e.g., <input type="search"> clearing its value), the stack stays put. Wizards with unsaved work should confirm or block before calling pop().

License

MIT