Disclosure
DisclosureAn accordion built on <details>, so it opens with no JavaScript and stays keyboard-operable by default.
Preview
What does an engagement usually look like?
Do you work with existing systems?
What happens after it ships?
Controls
Notes
Built on native <details>/<summary>. The browser already gives you the toggle, the keyboard behaviour and the accessibility tree; a div with onClick gives you none of it and has to earn them all back.
The marker is rotated with CSS on [open], so the state is never held in React and cannot desynchronise from the element.
Single-open mode is deliberately absent. Forcing one panel shut to open another is a preference dressed as a rule, and it costs the reader the comparison they opened two panels to make.
Source
ShowHide
import { cn } from "@/lib/cn";
/**
* Built on native <details>. The browser supplies the toggle, the keyboard
* behaviour and the accessibility tree; a div with onClick has to earn all
* three back, and usually only gets as far as the first.
*/
export function Disclosure({
summary,
defaultOpen = false,
bordered = true,
children,
}: {
summary: string;
defaultOpen?: boolean;
bordered?: boolean;
children: React.ReactNode;
}) {
return (
<details
open={defaultOpen}
className={cn("group", bordered && "border-b border-rule last:border-0")}
>
<summary
className={cn(
"flex cursor-pointer list-none items-center justify-between gap-4 py-4",
"text-[15px] text-ink marker:hidden [&::-webkit-details-marker]:hidden",
)}
>
{summary}
{/* Rotated off [open] in CSS, so the state lives on the element and
cannot drift from what React thinks it is. */}
<svg
width="14"
height="14"
viewBox="0 0 14 14"
fill="none"
aria-hidden="true"
className="shrink-0 text-ink-faint transition-transform duration-200 group-open:rotate-45"
>
<path d="M7 2v10M2 7h10" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" />
</svg>
</summary>
<div className="pb-5 text-[15px] leading-relaxed text-ink-muted">{children}</div>
</details>
);
}
const panels = [
{
summary: "What does an engagement usually look like?",
body: "A short paid discovery to map the problem, then a fixed scope for the first working slice. You see something running early rather than at the end.",
},
{
summary: "Do you work with existing systems?",
body: "Usually, yes. Most of the value is in the connective tissue between tools that already work, not in replacing them.",
},
{
summary: "What happens after it ships?",
body: "You own the code and the accounts. I stay on for as long as it is useful and no longer.",
},
];
export default function Demo({
count = "3",
openFirst = true,
bordered = true,
}: {
count?: string;
openFirst?: boolean;
bordered?: boolean;
}) {
const shown = panels.slice(0, Math.max(1, Number(count) || 1));
return (
<div className="w-full max-w-md">
{shown.map((panel, i) => (
<Disclosure
key={panel.summary}
summary={panel.summary}
defaultOpen={openFirst && i === 0}
bordered={bordered}
>
{panel.body}
</Disclosure>
))}
</div>
);
}
Next
Scroll revealNext step
Tell me where the business is losing time.
No pitch deck and no discovery-call funnel. Describe the bottleneck and I will tell you plainly whether it is worth building something for, and roughly what that would take.