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

@19h47/windowsplitter

v1.0.0

Published

Window splitter controller with accessibility in mind

Readme

@19h47/windowsplitter

Window splitter behaviour for UIs where a focusable role="separator" must resize a primary pane through JavaScript orchestration, while HTML remains the source of truth for ARIA.

Based on the W3C APG Window Splitter pattern: Window Splitter Pattern

Install

pnpm add @19h47/windowsplitter

HTML

HTML is the source of truth. Set role, tabindex, aria-orientation, aria-valuemin, aria-valuemax, aria-valuenow, labelling attributes, and aria-controls (primary pane) in the markup; the library reads them and mirrors layout onto the separator and primary pane.

aria-valuemin / aria-valuemax must match the real allowed range of the primary pane (not always 0100). If the pane cannot fully collapse, set min above 0.

Note on role: the APG documents role="separator". In practice, JAWS / NVDA / VoiceOver often handle a focusable role="slider" more reliably (discussion). The library does not enforce the role — pick what works for your AT testing.

<div class="panes">
	<div id="toc" class="pane pane--primary">Table of contents</div>
	<div
		role="separator"
		tabindex="0"
		aria-orientation="vertical"
		aria-valuemin="0"
		aria-valuemax="100"
		aria-valuenow="30"
		aria-controls="toc"
		aria-label="Table of Contents"
	></div>
	<div class="pane pane--secondary">Content</div>
</div>

JavaScript

import WindowSplitter from '@19h47/windowsplitter';

const $separator = document.querySelector('[role="separator"]');
const splitter = new WindowSplitter($separator);

splitter.init();

Options

type Orientation = 'horizontal' | 'vertical';
type Mode = 'resize' | 'clip' | 'none';

type SizeContext = {
	value: number;
	ratio: number;
	offset: number;
	length: number;
};

type Options = {
	orientation?: Orientation; // default: aria-orientation, then 'vertical'
	mode?: Mode; // default: 'resize'
	step?: number; // default: 1
	page?: number; // default: 10
	fixed?: boolean; // default: false (Enter-only when true)
	container?: HTMLElement | null; // default: el.parentElement
	formatSize?: (context: SizeContext) => string; // default: `${value}%`
	formatValue?: (value: number) => string; // default: String(value) → aria-valuetext
};

| Option | Description | | ------------- | --------------------------------------------------------------------------- | | orientation | vertical = left/right move; horizontal = up/down move | | mode | resize sets primary width/height; clip sets clip-path; none events only | | step | Arrow key increment | | page | PageUp / PageDown / Shift+Arrow increment | | fixed | Toggle-only splitter (no arrow keys; pointer toggles like Enter) | | container | Element used for drag bounds (defaults to parent) | | formatSize | CSS size for the primary pane in resize mode (unit, not a label) | | formatValue | aria-valuetext string (localization, units in the announced name) |

// CSS pixels instead of percent
new WindowSplitter($separator, {
	formatSize: ({ offset }) => `${offset}px`,
});

// French typography for screen readers (space before %)
new WindowSplitter($separator, {
	formatValue: value => `${value} %`,
});

Comparison images (mode: 'clip')

Same interaction as a before/after slider: overlay panes and clip the primary (before) layer.

const splitter = new WindowSplitter($separator, {
	mode: 'clip',
	step: 5,
	page: 20,
});

splitter.init();

Keyboard Support

| Key | Function | | ----------- | ------------------------------------------------------------------------ | | Left Arrow | Moves a vertical splitter to the left. | | Right Arrow | Moves a vertical splitter to the right. | | Up Arrow | Moves a horizontal splitter up. | | Down Arrow | Moves a horizontal splitter down. | | Enter | Collapses the primary pane, or restores the previous position. | | Home | Moves splitter to aria-valuemin (optional in APG). | | End | Moves splitter to aria-valuemax (optional in APG). | | Page Up | Increases value by page. | | Page Down | Decreases value by page. | | Shift+Arrow | Uses page as the step size. |

A fixed splitter (fixed: true) omits arrow keys and only implements Enter (and pointer toggle).

F6 (optional pane cycling in the APG) is not implemented: it conflicts with browser chrome shortcuts and belongs to the app, not this controller.

Role, Property, State, and Tabindex Attributes

| Role | Attribute | Element | Usage | | ----------- | ----------------------- | ------- | --------------------------------------------------------------------- | | separator | | focusable splitter | Identifies the element as a window splitter. | | | tabindex="0" | splitter | Includes the separator in the page tab sequence. | | | aria-orientation | splitter | vertical or horizontal. | | | aria-valuenow | splitter | Current size of the primary pane. Written by the library on change. | | | aria-valuetext | splitter | Mirrored from aria-valuenow by the library (sync / setValue). | | | aria-valuemin | splitter | Minimum primary pane size (typically 0). | | | aria-valuemax | splitter | Maximum primary pane size (typically 100). | | | aria-controls | splitter | References the primary pane id. | | | aria-label / aria-labelledby | splitter | Accessible name matching the primary pane. | | | aria-disabled="true" | splitter | Disables pointer and keyboard interaction. Prefer tabindex="-1". |

Methods

| Method | Description | Arguments | | ------------- | ------------------------------------------------ | ------------------------------------------------------ | | init() | Bind events and sync from HTML | — | | destroy() | Remove event listeners | — | | sync() | Re-apply HTML ? layout | — | | setValue() | Set aria-valuenow / aria-valuetext and update layout | value, trigger (optional, default true) | | collapse() | Collapse primary pane to aria-valuemin | trigger (optional, default true) | | restore() | Restore previous value after collapse | trigger (optional, default true) | | toggle() | Collapse or restore | trigger (optional, default true) |

splitter.setValue(40);
splitter.collapse();
splitter.restore();
splitter.destroy();

Getters

| Getter | Type | Source | | ------------ | --------- | ------------------------------------------- | | value | number | aria-valuenow | | min | number | aria-valuemin | | max | number | aria-valuemax | | ratio | number | normalized 0…1 between min and max | | disabled | boolean | aria-disabled="true" | | collapsed | boolean | value === min | | vertical | boolean | orientation is vertical | | $primary | element | pane referenced by aria-controls | | $container | element | drag bounds container |

Sync classes & CSS variables

sync() / setValue() mirror state onto optional CSS hooks and keep aria-valuetext in sync with aria-valuenow:

| Class / variable | When / meaning | | ----------------------------- | --------------------------------------- | | is-focus | Separator has focus | | is-dragging | Pointer drag in progress | | is-collapsed | Primary pane at aria-valuemin | | is-disabled | aria-disabled="true" | | --windowsplitter-value | Current value on the container | | --windowsplitter-ratio | 0…1 ratio on the container | | --windowsplitter-offset | Pixel offset of the separator |

The library does not ship styles for these classes — use them in your own CSS.

Events

Events are dispatched on the separator element.

WindowSplitter.change

$separator.addEventListener('WindowSplitter.change', event => {
	const { value, min, max, ratio, collapsed } = event.detail;

	console.log({ value, min, max, ratio, collapsed });
});

Native-like input and change events are also fired when trigger is true.

Example

An interactive demo is at 19h47.github.io/19h47-windowsplitter when published.

Run tests with pnpm test.

References