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

@terrahq/side-panel

v1.0.1

Published

A lightweight JavaScript library for creating expandable side panel components with multiple visual variants

Readme

@terrahq/side-panel

A zero-dependency side panel component. Expands one panel at a time while the others collapse to show only their title. Supports horizontal, vertical, and responsive layouts with full keyboard accessibility. Animations use the native Web Animations API.

Live Demo

Installation

npm install @terrahq/side-panel

Quick Start

HTML

Add data-sidepanel-root to the container. Each panel needs a clickable header (data-sidepanel-hd) and a content area (data-sidepanel-bd) with an inner wrapper (data-sidepanel-wrapper).

<div class="c--side-panel-a" data-sidepanel-root role="tablist">

    <div class="c--side-panel-a__item" data-sidepanel-item="panel-1">
        <button class="c--side-panel-a__item__hd" data-sidepanel-hd
            id="tab-panel-1" role="tab" aria-controls="panel-panel-1"
            aria-expanded="true" aria-selected="true">
            <span class="c--side-panel-a__item__hd__title">Panel 1</span>
        </button>
        <div class="c--side-panel-a__item__bd" data-sidepanel-bd
            id="panel-panel-1" role="tabpanel" aria-labelledby="tab-panel-1"
            aria-hidden="false">
            <div class="c--side-panel-a__item__bd__wrapper" data-sidepanel-wrapper>
                <!-- Your content here -->
            </div>
        </div>
    </div>

    <div class="c--side-panel-a__item" data-sidepanel-item="panel-2">
        <button class="c--side-panel-a__item__hd" data-sidepanel-hd
            id="tab-panel-2" role="tab" aria-controls="panel-panel-2"
            aria-expanded="false" aria-selected="false">
            <span class="c--side-panel-a__item__hd__title">Panel 2</span>
        </button>
        <div class="c--side-panel-a__item__bd" data-sidepanel-bd
            id="panel-panel-2" role="tabpanel" aria-labelledby="tab-panel-2"
            aria-hidden="true">
            <div class="c--side-panel-a__item__bd__wrapper" data-sidepanel-wrapper>
                <!-- Your content here -->
            </div>
        </div>
    </div>

</div>

JavaScript

import SidePanel from '@terrahq/side-panel';

new SidePanel({
    element: document.querySelector('[data-sidepanel-root]'),
});

That's it! Everything else is optional.


Options

Every option has a sensible default. You only need to pass element.

element (required)

The root HTML element that wraps all your panels.

new SidePanel({
    element: document.querySelector('[data-sidepanel-root]'),
});

nameSpace

Default: 'sidepanel'

Changes the prefix for all data-* attributes. Useful if you have multiple independent panel systems on the same page and need them to not conflict with each other.

// Now the component looks for data-myns-item, data-myns-hd, etc.
new SidePanel({ element: el, nameSpace: 'myns' });

initialIndex

Default: 0

Which panel should be open when the page first loads. 0 is the first panel, 1 the second, etc.

// Start with the third panel open
new SidePanel({ element: el, initialIndex: 2 });

activeModifier

Default: '--is-active'

The CSS class suffix added to the active panel. The component reads the first class on each item and appends this modifier. For example, if your item has class c--side-panel-a__item, the active class becomes c--side-panel-a__item--is-active. Change this if your CSS uses a different naming convention.

new SidePanel({ element: el, activeModifier: '--active' });

duration

Default: 0.6

How long the open/close animation takes, in seconds. Lower values feel snappy, higher values feel smooth.

// Fast animation
new SidePanel({ element: el, duration: 0.3 });

ease

Default: 'cubic-bezier(0.76, 0, 0.24, 1)'

The easing curve for animations. Accepts any valid CSS easing string. The default gives a smooth ease-in-out feel.

// Linear (no acceleration)
new SidePanel({ element: el, ease: 'linear' });

// Bouncy
new SidePanel({ element: el, ease: 'cubic-bezier(0.34, 1.56, 0.64, 1)' });

mobileBreakpoint

Default: 810

The viewport width (in pixels) where the component switches from horizontal layout to vertical layout. Only matters when direction is 'responsive'. If the browser window is narrower than this value, panels stack vertically. If wider, they sit side by side.

// Switch to vertical below 1024px
new SidePanel({ element: el, mobileBreakpoint: 1024 });

direction

Default: 'responsive'

Controls the layout mode:

  • 'responsive' — horizontal on desktop, vertical on mobile (switches at mobileBreakpoint)
  • 'horizontal' — always side by side, even on small screens
  • 'vertical' — always stacked, like a classic accordion
// Always vertical, ignores screen size
new SidePanel({ element: el, direction: 'vertical' });

hideTitleOnActive

Default: true

When true, the title/header of the active panel is hidden during the animation, giving more room to the content. When false, the title stays visible even when the panel is open. Set to false if your design wants the title always showing.

// Keep titles visible
new SidePanel({ element: el, hideTitleOnActive: false });

scrollToActive

Default: null

Only works in vertical mode. When set to a number, the page scrolls to the newly opened panel after the animation finishes — but only if the panel title has scrolled above the viewport. The number is the offset in pixels from the top of the screen. Set to null to disable.

// Scroll to the active panel with 100px offset from top
new SidePanel({ element: el, scrollToActive: 100 });

onComplete

Default: null

A callback that fires once, right after the component finishes initializing. Useful for hiding a loading state or running setup code that depends on the panel being ready.

new SidePanel({
    element: el,
    onComplete: (instance) => {
        console.log('SidePanel is ready!', instance.activeIndex);
    },
});

onChange

Default: null

A callback that fires every time the active panel changes. Receives the new index, the new active DOM element, and the instance. Useful for syncing other UI, tracking analytics, or triggering secondary animations.

new SidePanel({
    element: el,
    onChange: (index, item, instance) => {
        console.log(`Switched to panel ${index}`);
    },
});

Data Attributes on the Container

Instead of passing JavaScript options, you can configure behavior directly in HTML using data attributes on the data-sidepanel-root element. These are read by the mounting script.

| Attribute | What it does | | --- | --- | | data-hide-title="false" | Keep panel titles visible when active (default is "true", which hides them) | | data-direction="vertical" | Force vertical layout. Use "horizontal" for always horizontal, or omit for responsive | | data-breakpoint="1024" | Change the mobile breakpoint (default 810) | | data-duration="0.3" | Speed up or slow down animations (default 0.6 seconds) | | data-ease="linear" | Change the animation curve (default "cubic-bezier(0.76, 0, 0.24, 1)") | | data-initial-index="2" | Which panel starts open (default 0, the first one) | | data-scroll-to-active="100" | In vertical mode, scroll to the active panel with this offset from the top. Omit to disable |

Example:

<div class="c--side-panel-a"
    data-sidepanel-root
    data-direction="vertical"
    data-hide-title="false"
    data-scroll-to-active="100"
    role="tablist">
    ...
</div>

Data Attributes on Children

These go on elements inside the container. All use the nameSpace prefix (default: sidepanel).

| Attribute | Where to put it | What it does | | --- | --- | --- | | data-sidepanel-item="my-id" | Each panel wrapper | Identifies a panel. The "my-id" value is used by external controls to target this panel | | data-sidepanel-hd | The clickable title/button | Marks the element as the panel header. Clicking it opens this panel | | data-sidepanel-bd | The content container | Marks the collapsible content area. This is what gets animated open/closed | | data-sidepanel-wrapper | A div inside bd | Inner wrapper for the content. JS calculates its width in horizontal mode so the content doesn't overflow | | data-sidepanel-control="my-id" | Any element on the page | An external button that opens the panel with the matching ID. Can live anywhere on the page, not just inside the component |


External Controls

You can place buttons anywhere on the page that open a specific panel. Just give them data-sidepanel-control with the panel's ID:

<!-- These can be in a nav, a sidebar, anywhere -->
<button data-sidepanel-control="panel-1">Go to Panel 1</button>
<button data-sidepanel-control="panel-2">Go to Panel 2</button>

Direction Modes

Responsive (default)

Horizontal above the breakpoint, vertical below it. No extra attributes needed.

<div class="c--side-panel-a" data-sidepanel-root>

Always Horizontal

Panels always sit side by side, even on mobile.

<div class="c--side-panel-a" data-sidepanel-root data-direction="horizontal">

Always Vertical

Panels always stack like a classic accordion.

<div class="c--side-panel-a" data-sidepanel-root data-direction="vertical">

Add data-scroll-to-active="100" to automatically scroll to the active panel with a 100px offset from the top (only in vertical mode, only when the panel title is above the viewport).


API

instance.setActive(index)

Open a panel from your code. Useful for "next/prev" buttons or responding to other events.

instance.setActive(2); // Opens the third panel

instance.destroy()

Stops all animations and cleans up internal references. Call this if you're removing the component from the DOM (e.g., in a SPA on route change).

instance.destroy();

instance.activeIndex

Read which panel is currently open. It's a number (0-based index).

console.log(instance.activeIndex); // 0

Accessibility

The component follows the WAI-ARIA tabs pattern. You add the ARIA attributes in your HTML, and the JS keeps them in sync automatically:

  • aria-expanded and aria-selected on buttons are updated when panels change
  • aria-hidden on content areas is toggled
  • tabindex="-1" is added to all focusable elements inside closed panels, so keyboard users can't tab into hidden content

SCSS

@use "sass:map";

.c--side-panel-a {
    width: 100%;
    overflow: hidden;

    @media all and ($viewport-type: $tabletm) {
        display: flex;
        max-height: 80vh;
    }

    &__item {
        overflow: hidden;
        border: 1px solid map.get($color-options, i);
        flex: 0 0 auto;
        min-width: 0;

        @media all and ($viewport-type: $tabletm) {
            display: flex;
        }

        &__hd {
            display: block;
            width: 100%;
            text-align: left;
            white-space: nowrap;

            @media all and ($viewport-type: $tabletm) {
                width: auto;
                writing-mode: vertical-lr;
                transform: rotate(180deg);
            }

            &__title {
                display: block;
                padding: $measure*2;
            }
        }

        &__bd {
            overflow: hidden;
            max-height: 100%;
            scrollbar-gutter: stable;

            &__wrapper {
                width: 100%;
                padding: $measure*3 $measure*2;
            }
        }
    }

    &[data-direction="vertical"] {
        display: block;

        .c--side-panel-a {
            &__item {
                display: block;

                &__hd {
                    width: 100%;
                    writing-mode: horizontal-tb;
                    transform: none;
                }
            }
        }
    }
}

Dependencies

None. Animations use the native Web Animations API.

License

MIT