Skip to content
Andrew SmithSolutions
← Component library

Disclosure

Disclosure

An accordion built on <details>, so it opens with no JavaScript and stays keyboard-operable by default.

AccordiondetailsNo JSCSS only — no dependency

Preview

What does an engagement usually look like?
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.
Do you work with existing systems?
Usually, yes. Most of the value is in the connective tissue between tools that already work, not in replacing them.
What happens after it ships?
You own the code and the accounts. I stay on for as long as it is useful and no longer.

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

Show
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 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.