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

@data-slot/resizable

v1.0.2

Published

Headless resizable panel groups for vanilla JavaScript. Accessible, unstyled, tiny.

Readme

@data-slot/resizable

Headless resizable panel groups for vanilla JavaScript. Accessible, unstyled, tiny.

Inspired by react-resizable-panels so that shadcn/ui-style component libraries can use it as a drop-in primitive.

Installation

npm install @data-slot/resizable

Quick Start

<div data-slot="resizable" data-direction="horizontal">
  <div data-slot="resizable-panel" data-default-size="50" data-min-size="20">Left</div>
  <div data-slot="resizable-handle" aria-label="Resize panels"></div>
  <div data-slot="resizable-panel" data-default-size="50">Right</div>
</div>

<script type="module">
  import { create } from "@data-slot/resizable";

  const controllers = create();
</script>

API

Initialization

create(scope?)

Auto-discover and bind all resizable groups in a scope (defaults to document).

import { create } from "@data-slot/resizable";

const controllers = create(); // Returns ResizableController[]

createResizable(root, options?)

Create a controller for a specific element.

import { createResizable } from "@data-slot/resizable";

const resizable = createResizable(element, {
  direction: "horizontal",
  keyboardResizeBy: 10,
  onLayoutChange: (layout) => console.log(layout),
});

Slots

| Slot | Description | | ---- | ----------- | | resizable | Root flex container for the group | | resizable-panel | A panel with a percentage size | | resizable-handle | Focusable separator between adjacent panels |

<div data-slot="resizable">
  <div data-slot="resizable-panel">A</div>
  <div data-slot="resizable-handle" aria-label="Resize panels"></div>
  <div data-slot="resizable-panel">B</div>
</div>

A group needs at least one resizable-panel, and exactly one resizable-handle between each adjacent pair of panes.

Data Attributes

Options can also be set via data attributes. JS options take precedence.

On the root:

| Attribute | Type | Default | Description | | ------------------------- | ------ | ------------ | -------------------------------- | | data-direction | string | horizontal | Layout axis | | data-keyboard-resize-by | number | 10 | Percent moved per arrow keypress |

On each resizable-panel:

| Attribute | Type | Default | Description | | --------------------- | ------- | ---------- | ----------------------- | | data-default-size | number | even split | Initial size (%) | | data-min-size | number | 0 | Minimum size (%) | | data-max-size | number | 100 | Maximum size (%) | | data-collapsible | boolean | false | Pane can collapse | | data-collapsed-size | number | 0 | Size (%) when collapsed |

Panel sizes and constraints must be between 0 and 100. minSize must not exceed maxSize, and a collapsible panel's collapsedSize must not exceed minSize. The constraints must allow a layout totaling 100%; initialization throws if they cannot. A collapsible panel can occupy its collapsed size or any size between its minimum and maximum.

Options

| Option | Type | Default | Description | | ------------------ | ---------------------------- | -------------- | -------------------------------- | | direction | "horizontal" \| "vertical" | "horizontal" | Layout axis | | keyboardResizeBy | number | 10 | Percent moved per arrow keypress | | onLayoutChange | (layout: number[]) => void | undefined | Called when the layout changes |

Controller

Pane indices start at zero. setLayout() requires one finite, non-negative number per panel and a positive total; it normalizes the values to 100% and applies the panel constraints. Invalid layouts throw without changing the current layout. resizePane() requires a finite size. Indexed mutation methods and getSize() throw for an invalid pane index.

| Method/Property | Description | | ------------------------- | ------------------------------------------------------ | | layout | Current layout as number[] of percentages (readonly) | | setLayout(sizes) | Set the full layout (validated/clamped) | | resizePane(index, size) | Resize a pane to size% | | collapse(index) | Collapse a collapsible pane | | expand(index) | Expand a collapsed pane | | isCollapsed(index) | Whether a pane is collapsed | | isExpanded(index) | Whether a pane is expanded | | getSize(index) | Current size (%) of a pane | | destroy() | Cleanup all listeners and global styles |

Events

Outbound Events

element.addEventListener("resizable:change", (e) => {
  console.log("Layout:", e.detail.layout);
});

element.addEventListener("resizable:dragging", (e) => {
  console.log("Dragging:", e.detail.dragging);
});

Inbound Events

| Event | Detail | Description | | --------------- | ---------------------- | ------------------------------- | | resizable:set | { layout: number[] } | Set the layout programmatically |

element.dispatchEvent(new CustomEvent("resizable:set", { detail: { layout: [30, 70] } }));

Styling

The component sets flex styles on the root and panels directly. Give the group a height and the handles a visible width or height. Use data-* attributes for visual styling:

/* Style the handle */
[data-slot="resizable-handle"] {
  width: 4px;
  background: #ccc;
}

[data-slot="resizable"][data-direction="vertical"] [data-slot="resizable-handle"] {
  width: 100%;
  height: 4px;
}

/* Active drag / keyboard focus */
[data-slot="resizable-handle"][data-active] {
  background: #2563eb;
}

/* Collapsed pane */
[data-slot="resizable-panel"][data-collapsed] {
  opacity: 0;
}

Add a CSS transition on flex-grow for animated collapse/expand:

[data-slot="resizable-panel"] {
  transition: flex-grow 0.2s ease;
}

Keyboard Navigation

Focus a handle with Tab before using these keys:

| Key | Action | | --- | ------ | | ArrowLeft / ArrowRight | Resize a horizontal group by keyboardResizeBy (10% by default) | | ArrowUp / ArrowDown | Resize a vertical group by the same step | | Shift + arrow key | Move to the limit in that direction | | Home / End | Minimize / maximize the preceding panel within constraints | | Enter | Toggle collapse of the preceding panel when it is collapsible | | F6 / Shift + F6 | Focus the next / previous handle, wrapping within the group |

Accessibility

The component automatically handles:

  • role="separator" on each handle
  • aria-orientation (perpendicular to the layout direction)
  • aria-controls linking each handle to its preceding pane
  • aria-valuemin / aria-valuemax / aria-valuenow reflecting live constraints
  • Unique ID generation for the root, panes, and handles

Give each handle an accessible name with aria-label or aria-labelledby.

Behavior

Switching input methods

Keyboard resizing and valid imperative layout updates end an active drag before applying the change. Subsequent pointer movement has no effect until a new drag starts. Only one group can drag at a time in each document.

All input methods remember a panel's size immediately before it collapses. expand() and Enter restore that size, subject to the other panels' constraints. An initially collapsed panel expands to its minimum size when no prior size exists.

Persisting Layout

There is no built-in persistence, but reading and restoring the split is a one-liner:

import { createResizable } from "@data-slot/resizable";

const el = document.querySelector('[data-slot="resizable"]');
const saved = localStorage.getItem("layout:sidebar");

const resizable = createResizable(el, {
  onLayoutChange(layout) {
    localStorage.setItem("layout:sidebar", JSON.stringify(layout));
  },
});

if (saved) resizable.setLayout(JSON.parse(saved));

License

MIT