- Accordion
- Advanced Toast
- AlertMessage
- ArrowButton
- Arrows
- Avatar
- Backdrop
- Basic Toast
- Breadcrumb
- Button
- CardBackground
- Card
- Carousel
- Chapters
- ChatBody
- ChatButton
- ChatHeader
- ChatInput
- ChatMessageActions
- ChatMessageEditor
- ChatMessage
- ChatTypingIndicator
- ChatWindowCsatPrompt
- ChatWindowQuote
- ChatWindow
- CheckboxSelect
- Checkbox
- Checkboxes
- Chip
- ChoiceExpander
- Clickable
- ContentReview
- Countdown
- CriteriaDates
- CriteriaParticipants
- CriteriaTransports
- Date Field
- Datepicker
- Dropdown
- ElasticHeight
- ExpandableCard
- ExpandableText
- Filter
- FloatingLabelTextField
- Form Control
- Form Label
- Frame
- HamburgerIcon
- Heading
- Image
- JourneyCard
- LayerHeader
- Layer
- Link
- Loader
- Number Field
- PageNavigation
- Pagination
- Paragraph
- Password
- Phone Field
- Popin
- Portal
- ProductThumbnail
- Promises
- Prose
- Radio Group
- RadioSelect
- Radio
- Range
- Scrollbar
- Select
- SidebarLayout
- SliderFrame
- SparkleWrapper
- Spinner
- Switch
- Table
- Tabs
- Tag
- TextField
- Toggle
- Tooltip
- ValidationMessage
Trident UI is ClubMed's open-source component library built on React and Tailwind CSS. It provides accessible, composable primitives for building consistent product interfaces.
Run npx shadcn@latest add https://develop.trident-ui.pro.clubmed/r/accordion.json to add the component to your project.
Yes — pass allowMultiple to let any number of panels be open at the same time. By default only one panel is open at once.
'use client';
import { Accordion } from '@/ui/Accordion';
const items = [
{
id: 'what-is-trident',
header: (
<span className="flex w-full items-center justify-between py-16 text-left font-semibold">
What is Trident UI?
<span className="ml-8 text-sm opacity-60 transition-transform in-[[aria-expanded='true']]:rotate-180">
▾
</span>
</span>
),
content: (
<p className="pb-16 text-sm text-black/70">
Trident UI is ClubMed's open-source component library built on React and Tailwind CSS.
It provides accessible, composable primitives for building consistent product interfaces.
</p>
),
},
{
id: 'how-to-install',
header: (
<span className="flex w-full items-center justify-between py-16 text-left font-semibold">
How do I install it?
<span className="ml-8 text-sm opacity-60 transition-transform in-[[aria-expanded='true']]:rotate-180">
▾
</span>
</span>
),
content: (
<p className="pb-16 text-sm text-black/70">
Run{' '}
<code className="rounded bg-black/5 px-4 py-1 font-mono text-xs">
npx shadcn@latest add https://develop.trident-ui.pro.clubmed/r/accordion.json
</code>{' '}
to add the component to your project.
</p>
),
},
{
id: 'allow-multiple',
header: (
<span className="flex w-full items-center justify-between py-16 text-left font-semibold">
Does it support multiple open panels?
<span className="ml-8 text-sm opacity-60 transition-transform in-[[aria-expanded='true']]:rotate-180">
▾
</span>
</span>
),
content: (
<p className="pb-16 text-sm text-black/70">
Yes — pass{' '}
<code className="rounded bg-black/5 px-4 py-1 font-mono text-xs">allowMultiple</code> to let
any number of panels be open at the same time. By default only one panel is open at once.
</p>
),
},
];
export function AccordionDemo() {
return (
<div className="w-full max-w-lg space-y-12">
<Accordion
items={items}
className="divide-y divide-black/10 rounded-12 border border-black/10"
aria-label="FAQ"
/>
</div>
);
}
Installation
npx shadcn@latest add https://develop.trident-ui.pro.clubmed/r/accordion.json
Usage
'use client';
import { Accordion } from '@/docs/lib/ui/accordion';
const items = [
{
header: <span>What is Trident UI?</span>,
content: <p>Trident UI is ClubMed's open-source React component library.</p>,
},
{
header: <span>How do I install it?</span>,
content: <p>Run npx shadcn@latest add … to add any component to your project.</p>,
},
];
export function Component() {
return <Accordion items={items} aria-label="FAQ" />;
}Behaviour
Single-open (default)
By default, opening a panel automatically closes the previously open one. This is the classic accordion pattern: at most one panel is expanded at any time.
<Accordion items={items} />Multi-open
Pass allowMultiple to let the user open any number of panels independently. Each panel's
collapse / expand state is tracked separately.
- ✈ CDG → DXB — 6 h 30 min — Emirates EK 76
- ✈ DXB → DPS — 8 h 05 min — Emirates EK 368
- Total travel time: 17 h 25 min (1 stop)
- ✈ DPS → SIN — 2 h 35 min — Singapore Airlines SQ 938
- ✈ SIN → CDG — 13 h 05 min — Singapore Airlines SQ 336
- Total travel time: 17 h 55 min (1 stop)
- 🧳 1 × 23 kg checked bag included per passenger
- 👜 1 × 7 kg carry-on included
- 🔄 Changes allowed with fee before departure
- ❌ Non-refundable after ticketing
'use client';
import { Accordion } from '@/ui/Accordion';
const items = [
{
header: (
<span className="flex w-full items-center justify-between py-16 text-left font-semibold">
Outbound — Paris CDG → Bali DPS
<span className="ml-8 text-sm opacity-60 transition-transform in-[[aria-expanded='true']]:rotate-180">
▾
</span>
</span>
),
content: (
<ul className="space-y-8 pb-16 text-sm text-black/70">
<li>✈ CDG → DXB — 6 h 30 min — Emirates EK 76</li>
<li>✈ DXB → DPS — 8 h 05 min — Emirates EK 368</li>
<li className="font-medium text-black">Total travel time: 17 h 25 min (1 stop)</li>
</ul>
),
},
{
header: (
<span className="flex w-full items-center justify-between py-16 text-left font-semibold">
Return — Bali DPS → Paris CDG
<span className="ml-8 text-sm opacity-60 transition-transform in-[[aria-expanded='true']]:rotate-180">
▾
</span>
</span>
),
content: (
<ul className="space-y-8 pb-16 text-sm text-black/70">
<li>✈ DPS → SIN — 2 h 35 min — Singapore Airlines SQ 938</li>
<li>✈ SIN → CDG — 13 h 05 min — Singapore Airlines SQ 336</li>
<li className="font-medium text-black">Total travel time: 17 h 55 min (1 stop)</li>
</ul>
),
},
{
header: (
<span className="flex w-full items-center justify-between py-16 text-left font-semibold">
Baggage & fare conditions
<span className="ml-8 text-sm opacity-60 transition-transform in-[[aria-expanded='true']]:rotate-180">
▾
</span>
</span>
),
content: (
<ul className="space-y-8 pb-16 text-sm text-black/70">
<li>🧳 1 × 23 kg checked bag included per passenger</li>
<li>👜 1 × 7 kg carry-on included</li>
<li>🔄 Changes allowed with fee before departure</li>
<li>❌ Non-refundable after ticketing</li>
</ul>
),
},
];
export function AccordionMultipleDemo() {
return (
<div className="w-full max-w-lg space-y-12">
<Accordion
items={items}
allowMultiple
className="divide-y divide-black/10 rounded-12 border border-black/10"
aria-label="Flight details"
/>
</div>
);
}
Trigger wrapper
Pass triggerWrapper — a function (children: ReactNode) => ReactNode — to render a shared
layout around every item's trigger. This lets you declare the flex row, padding, chevron, or any
other chrome in one place instead of repeating it in every AccordionItem.header.
Trident UI is ClubMed's open-source component library built on React and Tailwind CSS. It provides accessible, composable primitives for building consistent product interfaces.
Run npx shadcn@latest add https://develop.trident-ui.pro.clubmed/r/accordion.json to add the component to your project.
Yes — pass allowMultiple to let any number of panels be open at the same time.
'use client';
import { type ReactNode } from 'react';
import { Accordion } from '@/ui/Accordion';
const items = [
{
id: 'what-is-trident',
header: 'What is Trident UI?',
content: (
<p className="pb-16 text-sm text-black/70">
Trident UI is ClubMed's open-source component library built on React and Tailwind CSS.
It provides accessible, composable primitives for building consistent product interfaces.
</p>
),
},
{
id: 'how-to-install',
header: 'How do I install it?',
content: (
<p className="pb-16 text-sm text-black/70">
Run{' '}
<code className="rounded bg-black/5 px-4 py-1 font-mono text-xs">
npx shadcn@latest add https://develop.trident-ui.pro.clubmed/r/accordion.json
</code>{' '}
to add the component to your project.
</p>
),
},
{
id: 'allow-multiple',
header: 'Does it support multiple open panels?',
content: (
<p className="pb-16 text-sm text-black/70">
Yes — pass{' '}
<code className="rounded bg-black/5 px-4 py-1 font-mono text-xs">allowMultiple</code> to let
any number of panels be open at the same time.
</p>
),
},
];
function triggerWrapper(children: ReactNode) {
return (
<span className="flex w-full items-center justify-between py-16 text-left font-semibold">
Tab Item: {children}
<span className="ml-8 text-sm opacity-60 transition-transform in-[[aria-expanded='true']]:rotate-180">
▾
</span>
</span>
);
}
export function AccordionTriggerWrapperDemo() {
return (
<div className="w-full max-w-lg space-y-12">
<div>
Trigger: <span>Header</span>
</div>
<div>Wrapped trigger :{triggerWrapper('Header')}</div>
<Accordion
items={items}
triggerWrapper={triggerWrapper}
className="divide-y divide-black/10 rounded-12 border border-black/10"
aria-label="FAQ"
/>
</div>
);
}
When triggerWrapper is omitted, item.header renders directly inside the <button> — existing
behaviour is unchanged.
Composing styles
The component carries no visual styling of its own — it is intentionally naked. Apply
Tailwind classes (or any CSS) to the header / content ReactNodes, and use className
on the root <div> to frame the whole widget.
<Accordion
items={styledItems}
className="divide-y divide-black/10 rounded-12 border border-black/10"
aria-label="FAQ"
/>Keyboard navigation
The component implements the WAI-ARIA Accordion keyboard pattern.
| Key | Action |
|---|---|
Enter / Space | Toggle the focused panel |
Tab | Move focus to the next focusable element in the page |
Shift+Tab | Move focus to the previous focusable element |
ArrowDown | Move focus to the next accordion header (wraps around) |
ArrowUp | Move focus to the previous accordion header (wraps around) |
Home | Move focus to the first accordion header |
End | Move focus to the last accordion header |
Accessibility
- Each trigger is a native
<button type="button">— keyboard-operable out of the box. aria-expandedreflects the open/closed state on every button.aria-controlslinks each button to its panel by id.- Each panel carries
role="region"andaria-labelledbyso screen readers announce the panel's heading. - IDs are generated with React's
useId— unique even when multiple accordions render on the same page. - Arrow-key navigation moves focus among accordion headers only; interactive content inside a panel is reached with
Tabas usual.
API Reference
Accordion
| Prop | Type | Default | Description |
|---|---|---|---|
items | AccordionItem[] | required | Array of { header: ReactNode; content: ReactNode } pairs |
allowMultiple | boolean | false | When true, any number of panels can be open simultaneously; when false, opening a panel closes the previously open one |
triggerWrapper | (children: ReactNode) => ReactNode | undefined | Render function called with item.header for each item. Return value is rendered inside the trigger <button> in place of the bare header. Useful for sharing flex layouts, chevrons, or other chrome across all items |
className | string | undefined | CSS classes applied to the root <div> |
Extends all standard HTML div attributes (id, aria-label, data-*, etc.).
AccordionItem
| Field | Type | Description |
|---|---|---|
id | string | Optional stable identifier for the item. Used as the open-state key and React key. When omitted, an id is auto-generated from the accordion's useId token and the item's index |
header | ReactNode | Content rendered inside the trigger <button>. Can be a string, icon+label combo, or any React element |
content | ReactNode | Content revealed when the panel is expanded. Rendered inside an ElasticHeight wrapper so height animates smoothly |
Implementation notes
- Panels are animated via the
ElasticHeightcomponent (grid-rows-[0fr]↔grid-rows-[1fr]) with a 500 ms CSS transition — no JavaScript height calculations orResizeObserver. - Content stays in the DOM even when collapsed, matching the standard accordion pattern and ensuring SEO / screen-reader discoverability.
- Open state is fully internal (
useState<Set<number>>). There is no controlled /valueprop by design — the component is a layout primitive, not a form element. - Focus management uses a
refarray soArrowUp/Down/Home/Endcall.focus()imperatively. - When switching from
allowMultiple=truetoallowMultiple=falseat runtime, existing open panels are not auto-closed; the constraint applies on the next user interaction.