@get-set/gs-accordion
v1.0.4
Published
Get-Set Accordion
Maintainers
Readme
GSAccordion
A dependency-free, fully accessible accordion / collapse component that ships in four interchangeable flavours from a single codebase:
- a window factory —
window.GSAccordion(selectorOrElement, params) - a jQuery plugin —
$(selector).GSAccordion(params) - an HTMLElement prototype method —
element.GSAccordion(params) - a React component —
<GSAccordion>with a typed imperative ref
All four share the same actions/, constants/, helpers/ and types/ — so
behaviour, animation and accessibility are identical across every target.
Features
- Two modes —
single(one open at a time) ormultiple(independent). - Collapsible — allow all panels closed in single mode (or force one open).
- Beautiful animation — smooth height-slide + fade with configurable
animationDuration/animationEasing; honorsprefers-reduced-motion. - Indicator icons —
chevron,plus(morphs to minus),caret, or a fullycustomHTML glyph, positionedleftorright(RTL-aware), with an optional rotate/morph animation. - Five variants —
bordered,separated,filled,ghost,flush. - Per-item
disabled. - Theming —
light/dark/auto(followsprefers-color-scheme) plus a customaccentColor. - Sizes —
sm/md/lg. - RTL layout.
- Full keyboard support — Arrow Up/Down (+ Left/Right, RTL-aware), Home, End, Enter, Space, per the WAI-ARIA accordion pattern.
- Full ARIA — header buttons carry
aria-expanded+aria-controls; panels arerole="region"labelled by their header. - Callbacks —
beforeToggle(cancelable),onChange,onOpen,onClose. - Items two ways — author them as markup (header + panel per item) or as a config array.
- React-only
gsx— per-instance scoped styles.
Installation
npm i @get-set/gs-accordionPackage entry points
| Environment | Resolves to | What you get |
| ------------------------ | ----------------------- | ---------------------------------------------- |
| import ... from | dist/components/… | The React component + named exports + types |
| require(...) / <script> | dist-js/bundle.js | window.GSAccordion + jQuery + prototype |
The bundled dist-js/bundle.js self-registers all three native flavours: it
assigns window.GSAccordion, and (when jQuery is present) $.fn.GSAccordion,
plus HTMLElement.prototype.GSAccordion.
Project layout
GSAccordion/
├─ GSAccordion.ts # window factory entry (native)
├─ actions/ # init, toggle, keyboard, animate, refresh, destroy
├─ constants/ # defaultParams, sizes
├─ helpers/ # layout, icons, uihelpers
├─ types/ # params, ref, global.d.ts
├─ components/
│ ├─ GSAccordion.tsx # React component
│ └─ styles/ # GSAccordion.css/.scss + GSAccordionCSS.ts
├─ styles/ # GSAccordion.css/.scss (published)
└─ example.html # native showcaseBuild
npm run build # build:js (webpack bundle) + build:react (tsc declarations)Tests
npm test # vitest (logic + native registry + React render/handle/gsx)Usage — Native JS (window factory)
The factory accepts either a CSS selector string or a DOM element as its first argument:
// by selector
const acc = window.GSAccordion('#faq', {
mode: 'single',
defaultOpen: 0,
variant: 'bordered',
icon: 'chevron'
});
// by element (identical result)
const el = document.getElementById('faq');
window.GSAccordion(el, { mode: 'multiple' });Markup form
Each direct child of the container is an item. Within it, the header can be a
[data-gs-accordion-header], a .gs-accordion-header, or the first heading /
button; the panel is [data-gs-accordion-panel], a .gs-accordion-panel, or
everything after the header.
<div id="faq">
<div>
<button data-gs-accordion-header>Question one</button>
<div data-gs-accordion-panel>Answer one.</div>
</div>
<div data-disabled="true">
<button data-gs-accordion-header>Coming soon</button>
<div data-gs-accordion-panel>Not yet.</div>
</div>
</div>Config-array form
Pass items and the plugin renders the markup for you:
window.GSAccordion('#features', {
mode: 'multiple',
variant: 'separated',
icon: 'plus',
iconPosition: 'left',
defaultOpen: ['perf'],
items: [
{ key: 'perf', header: 'Performant', panel: '<p>Tiny + fast.</p>' },
{ key: 'themed', header: 'Themeable', panel: '<p>Light/dark/auto.</p>' },
{ key: 'gone', header: 'Legacy', panel: '<p>…</p>', disabled: true }
]
});The instance registry — window.GSAccordionConfigue
Every native instance registers under its reference (auto-generated if
omitted). Retrieve it anywhere:
window.GSAccordion('#faq', { reference: 'faq' });
const acc = window.GSAccordionConfigue.instance('faq');
acc.open(1); // open the second item (by index)
acc.toggle('perf'); // toggle by key
acc.getOpen(); // -> ['perf']Native instance methods: open, close, toggle, openAll,
closeAll, getOpen, isOpen, refresh, destroy.
acc.open(); // open (respects mode: single opens the first)
acc.open(0); // open by index
acc.open(['a', 'b']); // open multiple by key
acc.close(1); // close by index/key
acc.close(); // close all
acc.toggle('a');
acc.openAll(); // expand every item (multiple mode)
acc.closeAll();
acc.getOpen(); // string[] of open keys
acc.isOpen('a'); // boolean
acc.refresh(); // rebuild from params, preserving open state
acc.destroy(); // remove listeners + ARIA, free the registry slotUsage — jQuery
<script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
<script src="node_modules/@get-set/gs-accordion/dist-js/bundle.js"></script>
<script>
$('#faq').GSAccordion({ mode: 'single', icon: 'caret' });
</script>Internally the jQuery adapter passes the element to window.GSAccordion, so
it shares the exact same engine.
Usage — HTMLElement prototype
document.querySelector('#faq').GSAccordion({
variant: 'filled',
accentColor: '#7c3aed'
});Usage — React
import GSAccordion, {
GSAccordionItem,
GSAccordionHandle
} from '@get-set/gs-accordion';
function FAQ() {
const ref = React.useRef<GSAccordionHandle>(null);
return (
<>
<button onClick={() => ref.current?.openAll()}>Expand all</button>
<GSAccordion ref={ref} mode="multiple" variant="separated" icon="plus">
<GSAccordionItem itemKey="a" header="What is it?">
A four-in-one accordion.
</GSAccordionItem>
<GSAccordionItem itemKey="b" header="Accessible?">
Fully — ARIA + keyboard.
</GSAccordionItem>
<GSAccordionItem itemKey="c" header="Coming soon" disabled>
Not yet.
</GSAccordionItem>
</GSAccordion>
</>
);
}Config-array in React
<GSAccordion
mode="single"
defaultOpen={0}
items={[
{ key: 'a', header: 'One', panel: '<p>Body one</p>' },
{ key: 'b', header: 'Two', panel: '<p>Body two</p>' }
]}
/>The imperative handle — GSAccordionHandle
| Method | Description |
| -------------------------- | ----------------------------------------------------- |
| open(key?) | Open item(s) by index/key. Omit to open (per mode). |
| close(key?) | Close item(s) by index/key. Omit to close all. |
| toggle(key) | Toggle the item by index/key. |
| openAll() | Expand every item (multiple mode). |
| closeAll() | Collapse every item. |
| getOpen() | string[] of the currently-open item keys. |
| isOpen(key) | Whether the given item is open. |
| refresh() | Re-measure open panels (after content changes). |
| destroy() | Unregister the instance from the global registry. |
Controlled vs uncontrolled
- Uncontrolled — use
defaultOpenand let the component manage state. - Controlled — pass
openKeys(an array of keys) and drive it yourself; the callbacks tell you what the user attempted.
The gsx prop (React only)
Inject per-instance scoped CSS. It is written under [data-key='<reference>']
on mount and removed on unmount:
<GSAccordion
reference="pricing"
gsx={{
'.gs-accordion-header': { fontWeight: '700' },
'.gs-accordion-item-open': { background: '#eef2ff' }
}}
>
…
</GSAccordion>Options
Every option is optional. Defaults are shown.
| Option | Type | Default | Description |
| ------------------- | ---------------------------------------------------------- | ------------------------------ | -------------------------------------------------------------------------- |
| reference | string | auto GUID | Registry key for the instance. |
| items | GSAccordionItem[] | — | Config-array items (else read from markup / children). |
| mode | 'single' \| 'multiple' | 'single' | One open at a time, or independent. |
| defaultOpen | string \| number \| Array<string \| number> | — | Items open on init (by index or key). Uncontrolled. |
| openKeys | Array<string \| number> (React only) | — | Controlled open keys. |
| collapsible | boolean | true | Single mode: allow all closed. false forces one open. |
| animation | boolean | true | Height-slide + fade. false disables motion. |
| animationDuration | number (ms) | 300 | Slide duration. |
| animationEasing | string | cubic-bezier(.4,0,.2,1) | CSS easing for the slide. |
| reducedMotion | 'auto' \| boolean | 'auto' | auto follows the OS; true always reduces; false never. |
| icon | 'chevron' \| 'plus' \| 'caret' \| 'custom' \| false | 'chevron' | Header indicator glyph. false hides it. |
| iconPosition | 'left' \| 'right' | 'right' | Which side the icon sits on (RTL-aware). |
| iconRotate | boolean | true | Rotate/morph the icon when its item opens. |
| customIcon | string (HTML) | — | Glyph used when icon: 'custom' (per-item icon overrides it). |
| variant | 'bordered' \| 'separated' \| 'filled' \| 'ghost' \| 'flush' | 'bordered' | Visual shell style. |
| size | 'sm' \| 'md' \| 'lg' | 'md' | Padding + font-size preset. |
| theme | 'light' \| 'dark' \| 'auto' | 'auto' | auto follows prefers-color-scheme. |
| accentColor | string | #2563eb | Accent (open icon, focus ring, highlights) — CSS var --gsa-accent. |
| rtl | boolean | false | Right-to-left layout. |
| className | string | — | Extra class on the root. |
| gsx | NestedCSS (React only) | — | Per-instance scoped styles. |
| beforeToggle | (e) => void \| boolean | — | Fired before a toggle. Return false to cancel. |
| onChange | (openKeys: string[], e) => void | — | Fired after any open-state change. |
| onOpen | (key: string, index: number) => void | — | Fired when an item opens. |
| onClose | (key: string, index: number) => void | — | Fired when an item closes. |
GSAccordionItem (config array)
| Field | Type | Description |
| ---------- | --------- | ------------------------------------------------- |
| key | string | Stable key (falls back to the index). |
| header | string | Header label (HTML allowed). |
| panel | string | Panel body (HTML allowed). |
| disabled | boolean | Disable the item. |
| open | boolean | Open on init (merged with defaultOpen). |
| icon | string | Per-item custom icon HTML (with icon: 'custom').|
GSAccordionToggleEvent (callback payload)
| Field | Type | Description |
| --------- | ------------- | -------------------------------------- |
| key | string | The item key. |
| index | number | The item index. |
| header | HTMLElement | The header button element. |
| panel | HTMLElement | The panel region element. |
| willOpen| boolean | true if opening, false if closing. |
Variant catalog
Every value of variant:
| Variant | Look |
| ----------- | ---------------------------------------------------------------- |
| bordered | Single rounded outline; items divided by hairlines. (default) |
| separated | Each item is its own rounded card with a gap; open card lifts. |
| filled | Each item is a soft-filled rounded block; open tint uses accent. |
| ghost | Borderless; only hairline dividers, accent on hover. |
| flush | Edge-to-edge, bottom-bordered rows with no side padding. |
Icon catalog
Every value of icon:
| Icon | Behaviour |
| --------- | ------------------------------------------------------------ |
| chevron | Down chevron; rotates 180° when open. (default) |
| caret | Solid triangle; rotates 180° when open. |
| plus | Plus sign; the vertical stroke collapses into a minus. |
| custom | Your own HTML (customIcon or the per-item icon). |
| false | No indicator. |
Size catalog
| Size | Header padding | Font size |
| ---- | -------------- | --------- |
| sm | 10 × 12 px | 13 px |
| md | 14 × 16 px | 15 px |
| lg | 18 × 20 px | 17 px |
Theme catalog
| Theme | Behaviour |
| ------- | ----------------------------------------------- |
| light | Always light surface. |
| dark | Always dark surface. |
| auto | Follows prefers-color-scheme. (default) |
Keyboard & accessibility
| Key | Action |
| -------------------------------- | ------------------------------------------ |
| Enter / Space | Toggle the focused item. |
| Arrow Down / Arrow Right* | Focus the next header (wraps). |
| Arrow Up / Arrow Left* | Focus the previous header (wraps). |
| Home | Focus the first header. |
| End | Focus the last header. |
* Left/Right are mirrored under rtl: true.
Each header exposes aria-expanded and aria-controls; each panel is
role="region" labelled by its header via aria-labelledby. Disabled items get
aria-disabled="true" and are skipped by roving focus.
Examples
Native — FAQ, single mode, first item open
window.GSAccordion('#faq', { mode: 'single', defaultOpen: 0 });Native — separated cards, multiple, plus icon (element arg)
const acc = window.GSAccordion(document.getElementById('features'), {
mode: 'multiple',
variant: 'separated',
icon: 'plus',
iconPosition: 'left',
items: [
{ key: 'a', header: 'Fast', panel: '<p>Tiny.</p>' },
{ key: 'b', header: 'Themed', panel: '<p>Dark mode.</p>' }
]
});
acc.openAll();React — controlled, dark theme, custom accent
function Controlled() {
const [open, setOpen] = React.useState<string[]>(['a']);
return (
<GSAccordion
mode="multiple"
theme="dark"
accentColor="#22d3ee"
openKeys={open}
onChange={(keys) => setOpen(keys)}
>
<GSAccordionItem itemKey="a" header="Alpha">A</GSAccordionItem>
<GSAccordionItem itemKey="b" header="Beta">B</GSAccordionItem>
</GSAccordion>
);
}React — cancelable toggle with beforeToggle
<GSAccordion
beforeToggle={(e) => {
if (e.key === 'locked' && e.willOpen) {
return false; // veto opening this item
}
}}
>
…
</GSAccordion>License
ISC © Get-Set
