Skip to content
KONIGI Patterns

AI Assistants / Answer formats / Response collapse

‹ 3 of 4 ›

Response collapse

People need to scan a conversation without long answers pushing everything else out of view.

Updated September 12, 2026

Problem

Long answers push questions and follow-up messages far apart, making a conversation difficult to scan.

Solution

Keep a useful summary visible and let people expand the details. Choose boundaries that preserve complete thoughts, such as sections, rather than cutting after an arbitrary number of lines.

Label the control so people know what it reveals. Make it keyboard accessible and expose whether the content is expanded to screen readers.

Avoid hiding text while someone is reading it as it arrives. Remember expanded sections when people move around the conversation. Make clear whether Copy includes the full answer, and check that search can reveal hidden matches.

Use when

Answers are long enough to make navigation difficult and can provide a summary that stands on its own.

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

People may miss an important qualification in hidden text. Expansion adds a step, and search or copying can behave unexpectedly if collapsed content is not handled carefully. A separate panel may work better for a full document.

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 provides a workspace for long documents. Reasoning disclosure hides extra detail about the assistant’s approach. Message turn becomes shorter when an answer is collapsed.

Structured output can make an answer easier to scan without hiding it. Conversation history helps people return to the exchange.

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 screenshots yet for this pattern. Browse Products to see other examples.