Collapse
A section that opens and closes under its heading, like one FAQ question, where the trigger and the content each receive the open state.
import { Collapse, CollapseProps, CollapseTriggerProps, CollapseContentProps } from '@salla.sa/twilight-theme-engine/components/collapse';In plain words
A collapse is a heading you click to show or hide the text under it, like the questions on an FAQ page. Several together make an accordion.
Like the other overlays, it keeps no state of its own; your component decides what is open. The catch is where that goes: Collapse.Trigger needs isOpen and onToggle (the function to call when it is clicked), and Collapse.Content needs isOpen. The outer Collapse asks for both too, but ignores them.
The content opens with a short height animation, without your code measuring anything.
Signature
function Collapse(props: CollapseProps): JSX.Element // <div class="overflow-hidden">
interface CollapseProps {
isOpen: boolean; // required, but not read
onToggle: () => void; // required, but not read
children: ReactNode;
className?: string;
}
Collapse.Trigger(props: CollapseTriggerProps) // <button type="button" aria-expanded>
Collapse.Content(props: CollapseContentProps) // grid rows 0fr ↔ 1fr, 200ms
interface CollapseTriggerProps extends ButtonHTMLAttributes<HTMLButtonElement> {
children: ReactNode;
isOpen?: boolean; // default false
onToggle?: () => void;
}
interface CollapseContentProps extends HTMLAttributes<HTMLDivElement> {
children: ReactNode;
isOpen?: boolean; // default false
}Try it live
Two to five working days in most cities.
Yes, within 14 days of delivery, unused and in its box.
Send us a message and we usually answer within a day.
import { useState } from 'react';
import { Collapse } from '@salla.sa/twilight-theme-engine/components/collapse';
export function Faq({ items }: { items: { id: string; question: string; answer: string }[] }) {
const [openId, setOpenId] = useState<string | null>(null);
return (
<div>
{items.map(({ id, question, answer }) => {
const isOpen = openId === id;
const toggle = () => setOpenId(isOpen ? null : id);
return (
<Collapse key={id} isOpen={isOpen} onToggle={toggle}>
<Collapse.Trigger isOpen={isOpen} onToggle={toggle} className="w-full py-3 font-bold">
{question}
</Collapse.Trigger>
<Collapse.Content isOpen={isOpen} inert={!isOpen}>
<p>{answer}</p>
</Collapse.Content>
</Collapse>
);
})}
</div>
);
}
Example
import { useState } from 'react';
import { Collapse } from '@salla.sa/twilight-theme-engine/components/collapse';
const QUESTIONS = [
{ id: 'delivery', question: 'How long does delivery take?', answer: 'Two to five working days.' },
{ id: 'returns', question: 'Can I return an item?', answer: 'Yes, within 14 days.' },
];
export function Faq() {
const [openId, setOpenId] = useState<string | null>(null);
return (
<div className="divide-y">
{QUESTIONS.map(({ id, question, answer }) => {
const isOpen = openId === id;
const toggle = () => setOpenId(isOpen ? null : id);
return (
<Collapse key={id} isOpen={isOpen} onToggle={toggle}>
{/* Trigger and Content do not read the outer Collapse: pass the state to each. */}
<Collapse.Trigger isOpen={isOpen} onToggle={toggle} className="w-full py-3 font-bold">
{question}
</Collapse.Trigger>
<Collapse.Content isOpen={isOpen} inert={!isOpen}>
<p>{answer}</p>
</Collapse.Content>
</Collapse>
);
})}
</div>
);
}
How it behaves
There is no context between the parts: the root only renders
<div className="overflow-hidden …">, and each part is controlled on its own. An accordion is one piece of state (the open id) in the parent.Collapse.Contentalways renders its children. Open it isgrid grid-rows-[1fr] opacity-100, closedgrid grid-rows-[0fr] opacity-0, withtransition-all duration-200and an inneroverflow-hiddenwrapper holding apb-2box. Because the text is in the HTML either way, answers are in the server-rendered page.Collapse.Triggeris a real<button type="button">, styled flat (border-0 bg-transparent p-0 text-start), witharia-expanded. Nothing setsaria-controls; add anidto the content andaria-controlsto the trigger yourself (both pass extra attributes through).It has no effects and needs no provider, so it renders the same on the server and in the browser. The same export is also at
@salla.sa/twilight-theme-engine/collapse.
Gotchas
Passing
isOpenandonToggleonly to the outerCollapsegives a section that never opens:Collapseignores them, the trigger has no click handler, and the content'sisOpendefaults tofalse. The engine's own example at the top ofCollapse.tsxdoes exactly this. PassisOpenandonToggletoCollapse.Trigger, andisOpentoCollapse.Content.The accordion example in the engine's
collapse/index.tsgives the triggeronToggle={() => setOpenId(item.id)}, so an open question can never be closed again. Toggle back tonullwhen it is already open, as in the example above.Closed content is only squeezed to zero height, not removed or hidden: links, buttons and inputs inside a closed section can still be reached with Tab and are read by screen readers. Add
inert={!isOpen}toCollapse.Content(it passes attributes through).Collapse.Triggerspreads your props after its own, so anonClickyou pass replacesonTogglerather than adding to it.