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

@archoleat/next-accordion

v1.2.0

Published

Animated React accordion using details and summary

Readme

Next Accordion

NPM Version NPM Downloads Specs Commitlint Editorconfig Prettier ESLint Remark Zero Deps AI

Table of Contents

Animated React accordion using details and summary

Installation

bun i -D @archoleat/next-accordion

Usage

Single item

Render a single collapsible panel:

import { AccordionItem } from '@archoleat/next-accordion';

const Example = () => (
  <AccordionItem trigger="Trigger">
    <p>Content</p>
  </AccordionItem>
);

export { Example };

List of items

Render a list of panels from an array of data:

import { Accordion } from '@archoleat/next-accordion';

const items = [
  { id: 1, trigger: 'First', content: <p>First content</p> },
  { id: 2, trigger: 'Second', content: <p>Second content</p> },
];

const Example = () => <Accordion exclusive items={items} />;

export { Example };

Accordion renders each item as an <li> inside a wrapping <ul> (with className applied to that <ul>), since a set of panels is semantically a list. Browsers apply default list styling (markers, indent) to <ul>, so reset it yourself — see Styling.

Compound components

Instead of items, compose AccordionItems directly as children. Accordion still coordinates the exclusive behavior between them:

import { Accordion, AccordionItem } from '@archoleat/next-accordion';

const Example = () => (
  <Accordion exclusive>
    <AccordionItem trigger="First">
      <p>First content</p>
    </AccordionItem>
    <AccordionItem trigger="Second">
      <p>Second content</p>
    </AccordionItem>
  </Accordion>
);

export { Example };

Give each AccordionItem a key when the list can be reordered or filtered; otherwise Accordion falls back to positional indexes to tell items apart. Accordion accepts either items or children, not both.

Controlling the open item from outside

In exclusive mode, which item is open lives inside Accordion by default. Pass openId (paired with onOpenIdChange) to read or drive it from outside — the same controlled/uncontrolled split AccordionItem uses for open/onOpenChange:

import { useState } from 'react';

import type { AccordionOpenIdType } from '@archoleat/next-accordion';
import { Accordion } from '@archoleat/next-accordion';

const items = [
  { id: 1, trigger: 'First', content: <p>First content</p> },
  { id: 2, trigger: 'Second', content: <p>Second content</p> },
];

const Example = () => {
  const [openId, setOpenId] = useState<AccordionOpenIdType>(1);

  return (
    <Accordion
      exclusive
      items={items}
      openId={openId}
      onOpenIdChange={setOpenId}
    />
  );
};

export { Example };

Uncontrolled usage works the same way defaultOpen does on AccordionItem: pass defaultOpenId for the initial open item and read subsequent changes through onOpenIdChange, without taking over the state yourself.

Styling

The library ships no CSS of its own beyond what's needed for the open/close animation, so every visual aspect is yours to style. The rendered markup gives you the following hooks:

  • Accordion's className targets the wrapping <ul>.

  • AccordionItem's className (a forwarded native details attribute) targets that item's <details>.

  • AccordionItem's triggerClassName and contentClassName target the <summary> and the panel wrapper <div> directly — no need to reach for descendant selectors.

  • trigger is a ReactNode, so wrap it in your own elements to style the label.

  • <details open> and a disabled item's data-disabled are both plain attributes, so Tailwind's open: and data-[disabled]: variants apply directly — no need for the icon function-of-isOpen form just to rotate a chevron.

The example below uses cn, the common clsx + tailwind-merge helper, to combine a shared base class with any per-item override:

import { Accordion } from '@archoleat/next-accordion';

import { cn } from './lib/cn';

const ChevronIcon = () => (
  <svg
    aria-hidden
    className="size-4 shrink-0 transition-transform duration-300 group-open:rotate-180"
    height="16"
    viewBox="0 0 16 16"
    width="16"
  >
    <path d="M4 6l4 4 4-4" fill="none" stroke="currentColor" strokeWidth="2" />
  </svg>
);

const items = [
  { id: 1, trigger: 'First', content: <p>First content</p> },
  { id: 2, trigger: 'Second', content: <p>Second content</p> },
];

const Example = () => (
  <Accordion
    className="m-0 flex list-none flex-col gap-2 p-0"
    exclusive
    items={items.map((item) => ({
      ...item,
      className: 'overflow-hidden rounded-lg border border-slate-200',
      contentClassName: 'p-4 pt-0',
      duration: 250,
      easing: 'ease-in-out',
      icon: <ChevronIcon />,
      triggerClassName: cn(
        'flex cursor-pointer items-center justify-between p-4',
        'aria-disabled:pointer-events-none aria-disabled:opacity-50',
      ),
    }))}
  />
);

export { Example };

Props

Accordion

| Prop | Type | Default | Description | | ----------- | ------------------- | ------- | --------------------------------------------------------------------------------------------------------------------------------------------------- | | items | AccordionItemDataType[] | — | Data rendered as a list of AccordionItems. Each entry accepts id (number), trigger, content, plus every AccordionItem prop below (except children, which maps to content). Mutually exclusive with children. | | children | ReactNode | — | AccordionItem elements composed directly (compound component pattern). Mutually exclusive with items. | | className | string | — | Class applied to the wrapping ul. Each item is rendered inside its own li. | | exclusive | boolean | false | When true, opening one item closes the others. | | openId | AccordionOpenIdType | — | Id of the currently open item (controlled). Only meaningful when exclusive is true. Pair with onOpenIdChange. | | defaultOpenId | AccordionOpenIdType | null | Initial open item id (uncontrolled). Only meaningful when exclusive is true. | | onOpenIdChange | (openId: AccordionOpenIdType) => void | — | Called when the open item changes in exclusive mode. |

AccordionItem

| Prop | Type | Default | Description | | ------------------------- | ---------------------------------------------- | -------------- | ----------------------------------------------------------------------------------------------- | | trigger | ReactNode | — | Content rendered inside the summary element. Required. | | children | ReactNode | — | Content rendered inside the accordion panel. Required. | | defaultOpen | boolean | false | Initial expanded state (uncontrolled). | | open | boolean | — | Expanded state (controlled). Pair with onOpenChange. | | onOpenChange | (isOpen: boolean) => void | — | Called when the trigger is clicked. | | disabled | boolean | false | Ignores clicks on the trigger. | | duration | number | 300 | Animation duration in milliseconds. | | easing | string | 'ease-in-out' | Animation easing, passed to the Web Animations API. | | icon | ReactNode \| (isOpen: boolean) => ReactNode | — | Rendered after trigger; the function form receives the open state. | | disableTriggerSelection | boolean | false | Prevents text selection on the trigger (e.g. on double-click). | | triggerClassName | string | — | Class applied to the summary element. | | contentClassName | string | — | Class applied to the panel wrapper div. |

All other native details attributes (id, className, and so on) are forwarded to the underlying <details> element. The animation is skipped in favor of an instant toggle when the user has prefers-reduced-motion enabled.

Types

@archoleat/next-accordion exports the prop types for both components, plus the shape used by items and by the open-item id, so you can type your own code around them without redeclaring the definitions.

AccordionOpenIdType (number | string | null) is the type of openId, defaultOpenId, and the argument passed to onOpenIdChange — see Controlling the open item from outside.

Type an items array built outside JSX with AccordionItemDataType:

import type { AccordionItemDataType } from '@archoleat/next-accordion';

const items: AccordionItemDataType[] = [
  { id: 1, trigger: 'First', content: <p>First content</p> },
  { id: 2, trigger: 'Second', content: <p>Second content</p> },
];

export { items };

Type a wrapper component with AccordionProps / AccordionItemProps:

import { Accordion, AccordionItem } from '@archoleat/next-accordion';
import type {
  AccordionItemProps,
  AccordionProps,
} from '@archoleat/next-accordion';

const FaqAccordion = (props: AccordionProps) => (
  <Accordion className="faq" {...props} />
);

const FaqItem = (props: AccordionItemProps) => (
  <AccordionItem duration={300} {...props} />
);

export { FaqAccordion, FaqItem };

Contributing

Please read CONTRIBUTING to start contributing.

License

This project is licensed under the MIT license.