What Panel does

Panel controls one element: it can be opened, closed, or toggled. The element animates its height (vertical axis) or width (horizontal axis) between 0 and its natural size.

Typical uses: show-more blocks, accordions, sidebars, drawers.

Not the same as PanelSet. Panel opens and closes a single element. PanelSet switches between several. See the Intro for the distinction.

Setup

Installing Panel

Terminal
npm install panelset

Using Panel

Markup

Add data-panel to your element and give it an id. Panels start closed by default (no class required). Wrap the content in .panel-wrapper.

Key Features

  • This is our most important feature ever!
HTML
<!-- Trigger: any element with aria-controls pointing to the panel id-->
<button aria-controls="markup-example">Toggle</button>

<!-- Panel element-->
<div id="markup-example" data-panel>
	<div class="panel-wrapper">
		<p>Panel content goes here.</p>
	</div>
</div>

aria-expanded is managed automatically. Panel sets it to true or false on every open and close.

Starting state

A Panel is closed by default. To start it open, add the is-open class to the panel element in your markup. That’s all. Panel reads it on init, removes inert, and syncs the trigger’s aria-expanded to match. No animation runs for the initial state.

Key Features

  • This is our most important feature ever!
HTML
<button aria-controls="starting-example">Toggle</button>

<div class="is-open" id="starting-example" data-panel>
	<div class="panel-wrapper">
		<p>Visible on load.</p>
	</div>
</div>

If you use persist or deepLink, a stored open/closed state takes precedence over the markup on return visits.

Initializing

Use the static init() method to initialize all matching elements at once, or create instances directly.

JS
// Initialize all [data-panel] elements
Panel.init();

// With options
Panel.init({ debug: true });

// Custom selector
Panel.init('[data-panel]', { transitions: false });

// Single instance
const panel = new Panel('#my-panel', { axis: 'vertical' });

// Single instance by element reference
const el = document.getElementById('my-panel');
const panel = new Panel(el, { autoFocus: 'first' });

Triggers and close buttons

Any element with aria-controls="<panel id here>" auto-binds a click handler on init. No extra JS needed. Add data-panel-close inside the panel for a close button.

HTML
<button aria-controls="my-panel" aria-expanded="false">Open</button>

<div id="my-panel" data-panel>
	<div class="panel-wrapper" role="dialog" aria-label="Options">
		<button data-panel-close>Close</button>
		<p>Sidebar content.</p>
	</div>
</div>

When a trigger and panel are adjacent siblings, you can skip the id / aria-controls pair entirely. Use data-panel-trigger on the button and Panel will generate and set up the ID automatically.

HTML
<button data-panel-trigger>Toggle</button>

<div data-panel>
	<div class="panel-wrapper">Content</div>
</div>

Public API

JS
const panel = new Panel('#my-panel');

panel.open();    // open
panel.close();   // close
panel.toggle();  // toggle open/closed

panel.isOpen;    // boolean (true if open or opening)

Async content

Attach a handler to delay opening until content is ready. If loading takes longer than loadingDelayms, a spinner appears.

JS
const panel = new Panel('#my-panel');

panel.onBeforeOpen(async (el, signal) => {
	const res = await fetch('/api/content', { signal });
	el.querySelector('.panel-wrapper').innerHTML = await res.text();
}, { once: true }); // load once, cache forever