Skip to content
KONIGI

AI Assistants / Output shape / Response collapse

3 of 4

Response collapse

One answer runs to eight screens and buries every other turn in the conversation.

Updated September 12, 2026

Problem

A thorough answer is two thousand words. It’s correct, well organised and the reason the product is useful, and it also means the question above it and the follow-up below it are now several screens apart with no way to see both.

Solution

Fold the body and leave the conclusion visible. Progressive disclosure works when the visible part is genuinely sufficient for the common case and the hidden part is genuinely secondary, and the whole difficulty is that a generated answer rarely arrives sorted that way.

Where to cut is the decision. Truncating at a fixed line count cuts mid-thought and frequently hides the conclusion, which is the one part nobody should have to expand for. Cutting on structure works better: keep the opening summary and the headings, fold the detail under each. Best of all is asking the model for a lead before the body, so there’s something designed to stand alone above the fold.

The control has to say what’s behind it. A bare “show more” gives the reader nothing to decide with. Naming the remainder, whether that’s a count of words, of steps, or of sections, converts a blind click into a judgement. Naming the remainder is what keeps the fold honest rather than merely tidy. A reader who can’t see the size of what’s behind it can’t tell whether the visible part is a summary or a scrap.

The disclosure pattern is small and specified, and the parts that get missed are the ones assistive technology depends on: a real button, an expanded state exposed programmatically, and a clear relationship between the control and the region it governs. Where the native details element fits, it gives all of that with no script.

Two failures recur. The first is collapsing while content is still streaming, so the reader watches text arrive and then sees it disappear behind a fold the instant generation finishes. Decide after the stream ends, or not at all. The second is losing the expanded state: a reader who expanded an answer, scrolled away and came back to find it folded again won’t expand it a second time.

The alternative worth weighing every time is leaving the answer whole. If an answer is long because it’s a document, it belongs in a panel rather than behind a fold, and the fold is a cheaper fix for a problem that’s a better one.

Use when

Answers are routinely long enough to bury the conversation, and a genuine summary can sit above the fold.

Don’t use when

The answer is short, or the whole of it’s the point. Folding a tight four-paragraph explanation adds a click and hides nothing worth hiding.

Trade-offs

Collapsing keeps the conversation navigable and hides content people paid for, and a reader who doesn’t expand may act on a partial answer believing it complete. It also breaks in-page search, since browser find won’t reach collapsed text, which is exactly how someone hunts through a long technical answer. Copying gets ambiguous, because a copy control on a folded answer has to decide between what’s visible and what exists. The fold also adds a decision to a surface that already asks for several.

Checklist

  • Does the visible part stand alone as an answer?
  • Is the conclusion above the fold?
  • Does the control name the size of what’s hidden?
  • Is the expanded state remembered when the reader scrolls away and back?
  • Does collapsing happen only after streaming finishes?
  • Can browser find reach the collapsed content?
  • Does copy take the whole answer or only the visible part?
  • Is the control a real button with its state exposed?
  • Would a panel serve better than a fold for this content?
  • What’s the length threshold, and was it measured against real answers?

Compare

Perplexity structures for the fold by leading with a short direct answer and keeping supporting detail below it, so the visible portion is designed to be sufficient rather than truncated. ChatGPT mostly declines to fold conversational answers and moves long editable output to a canvas instead. Length is treated as a container problem rather than a disclosure one. Claude applies the fold to reasoning rather than to answers, collapsing the part that is genuinely secondary and leaving the response whole. Slack truncates long messages with an expander inherited from the message list, so an AI answer is subject to the same limit as a colleague pasting a wall of text.

Artifact panel is the better answer when length comes from the output being a document. Reasoning disclosure is the same fold applied to the material most worth hiding. Message turn is what gets shorter when this works. Structured output often removes the need by making an answer scannable. Conversation history is what stays navigable as a result.

Response collapse anatomy A long answer folded after a lead that stands alone, with the control naming the size of what is hidden, and a second version cut at a fixed line count that hides the conclusion mid-sentence. Cut on structure, not on a line count a lead written to stand alone Show 4 more sections, 170 words cut at eight lines, mid-sentence show more the conclusion is behind the fold, and browser find cannot reach it 1 2 3 4 1 A LEAD THAT STANDS ALONE Ask the model for one, rather than hoping the first lines happen to serve. 2 NAME WHAT IS HIDDEN A bare show more gives the reader nothing to decide with. 3 A FIXED LINE COUNT Cuts mid-thought and frequently hides the one part nobody should have to expand for. 4 FIND CANNOT REACH IT Which is exactly how someone hunts through a long technical answer. If the answer is long because it is a document, a panel is the better fix and a fold is the cheap one.
Wireframe — the pattern's anatomy, not any one product's version of it

Implementation

Folding a long answer after a lead that stands on its own, with the control naming the size of what is hidden. Cutting at a fixed line count lands mid-sentence, and a bare show more leaves the reader nothing to decide with.

shadcn
npx shadcn@latest add button
npm
lucide-react
Tokens
--card--card-foreground--background--accent--accent-foreground--muted-foreground--border--chart-1

The build is slow because the type-check runs twice and the image pipeline never hits its cache. Fixing those two takes it from four minutes to about ninety seconds.

ResponseCollapse.tsxThe control counts what it hides. The fold is hidden until-found, so browser find opens it and the control follows.

import { useEffect, useRef, useState, type ReactNode } from "react";
import { ChevronDown, ChevronUp } from "lucide-react";
import { Button } from "@/components/ui/button";

type Section = { heading: string; body: string };

type Props = {
  /** Written to stand alone. Ask the model for one, rather than hoping the
   *  first lines happen to serve. */
  lead: string;
  /** Everything after the lead, cut on structure rather than on a line count. */
  sections: Section[];
  render?: (body: string) => ReactNode;
};

const words = (s: string) => s.trim().split(/\s+/).length;

export function ResponseCollapse({ lead, sections, render = (b) => <p>{b}</p> }: Props) {
  const [open, setOpen] = useState(false);
  const fold = useRef<HTMLDivElement>(null);
  const hidden = sections.reduce((n, s) => n + words(s.heading) + words(s.body), 0);

  // The fold stays in the DOM, hidden until found. Browser find then reaches
  // the conclusion, and beforematch tells us it did so the control agrees.
  // `hidden={!open}` in the JSX is what the server renders, so the fold
  // arrives closed; the effect then upgrades the attribute to until-found,
  // which React only knows how to set as a string via the DOM.
  useEffect(() => {
    const el = fold.current;
    if (!el) return;
    if (!open) el.setAttribute("hidden", "until-found");
    const onMatch = () => setOpen(true);
    el.addEventListener("beforematch", onMatch);
    return () => el.removeEventListener("beforematch", onMatch);
  }, [open]);

  return (
    <div className="text-sm leading-relaxed text-card-foreground">
      <p>{lead}</p>

      <Button variant="outline" size="sm" className="mt-3 border-chart-1 text-chart-1" onClick={() => setOpen(!open)} aria-expanded={open}>
        {open ? <ChevronUp className="size-4" /> : <ChevronDown className="size-4" />}
        {open ? "Show less" : `Show ${sections.length} more sections, ${hidden.toLocaleString()} words`}
      </Button>

      <div ref={fold} hidden={!open} className="mt-4 flex flex-col gap-4">
        {sections.map((s) => (
          <section key={s.heading}>
            <h4 className="font-medium">{s.heading}</h4>
            {render(s.body)}
          </section>
        ))}
      </div>
    </div>
  );
}

demo.tsxHow it is called: the lead and the sections as props. This is what the preview mounts.

import { ResponseCollapse } from "./ResponseCollapse";

/** The lead was asked for; the sections are the answer cut on its own headings. */
export default function Demo() {
  return (
    <div className="rounded-lg border bg-card p-4">
      <ResponseCollapse
        lead="The build is slow because the type-check runs twice and the image pipeline never hits its cache. Fixing those two takes it from four minutes to about ninety seconds."
        sections={[
          {
            heading: "The double type-check",
            body: "Both the lint script and the build script call tsc, and neither passes --incremental, so the whole project is checked twice from cold on every run. Run it once as its own step and let the build consume the emitted declarations.",
          },
          {
            heading: "The image cache key",
            body: "The pipeline keys its cache on the file's modified time, which the checkout step resets on every run, so every asset is re-encoded every time. Key on a content hash instead and the step drops from seventy seconds to under five.",
          },
          {
            heading: "The test step on the critical path",
            body: "Tests run before the bundle is produced, but nothing in the bundle depends on them. Move them into a parallel job that gates the deploy rather than the build.",
          },
          {
            heading: "What to change first",
            body: "The type-check, because it is a one-line change and it is the biggest single saving. Do the cache key second. Leave the test step until the other two are in, since it is a workflow change rather than a config one.",
          },
        ]}
      />
    </div>
  );
}
What it renders. Identical markup in both panes, with only the token values changing.

Examples

No captures reference this pattern yet. Captures arrive product by product; see Products for what's in the gallery so far.