cortexcn

Chain of Thought

Display step-by-step reasoning and collapsible thought processes in AI interfaces.

Loading…
import {  ChainOfThought,  ChainOfThoughtContent,  ChainOfThoughtItem,  ChainOfThoughtStep,  ChainOfThoughtTrigger,} from "@/components/ui/chain-of-thought";export default function ChainOfThoughtDemo() {  return (    <ChainOfThought className="max-w-lg">      <ChainOfThoughtStep defaultOpen>        <ChainOfThoughtTrigger>Understanding the request</ChainOfThoughtTrigger>        <ChainOfThoughtContent>          <ChainOfThoughtItem>            The user wants a weekly summary of their sales, grouped by region            and sorted by revenue.          </ChainOfThoughtItem>        </ChainOfThoughtContent>      </ChainOfThoughtStep>      <ChainOfThoughtStep>        <ChainOfThoughtTrigger>Querying the database</ChainOfThoughtTrigger>        <ChainOfThoughtContent>          <ChainOfThoughtItem>            Running an aggregate over the orders table for the last 7 days,            joining on the regions lookup.          </ChainOfThoughtItem>        </ChainOfThoughtContent>      </ChainOfThoughtStep>      <ChainOfThoughtStep>        <ChainOfThoughtTrigger>Composing the answer</ChainOfThoughtTrigger>        <ChainOfThoughtContent>          <ChainOfThoughtItem>            Formatting the results into a table and highlighting the top region            by revenue.          </ChainOfThoughtItem>        </ChainOfThoughtContent>      </ChainOfThoughtStep>    </ChainOfThought>  );}

Installation

npx shadcn@latest add @cortexcn/chain-of-thought
Install the following dependencies:
npm install radix-ui lucide-react
Add the collapsible component that Chain of Thought builds on:
npx shadcn@latest add @cortexcn/collapsible
Add the collapsible animations to your global CSS:
@theme {
  --animate-collapsible-down: collapsible-down 0.2s ease-out;
  --animate-collapsible-up: collapsible-up 0.2s ease-out;

  @keyframes collapsible-down {
    from {
      height: 0;
    }
    to {
      height: var(--radix-collapsible-content-height);
    }
  }

  @keyframes collapsible-up {
    from {
      height: var(--radix-collapsible-content-height);
    }
    to {
      height: 0;
    }
  }
}
Copy and paste the following code into your project.
components/ui/chain-of-thought.tsx
"use client";import {  Collapsible,  CollapsibleContent,  CollapsibleTrigger,} from "@/components/ui/collapsible";import { cn } from "@/lib/utils";import { ChevronDown, Circle } from "lucide-react";import React from "react";export type ChainOfThoughtItemProps = React.ComponentProps<"div">;export const ChainOfThoughtItem = ({  children,  className,  ...props}: ChainOfThoughtItemProps) => (  <div className={cn("text-muted-foreground text-sm", className)} {...props}>    {children}  </div>);export type ChainOfThoughtTriggerProps = React.ComponentProps<  typeof CollapsibleTrigger> & {  leftIcon?: React.ReactNode;  swapIconOnHover?: boolean;};export const ChainOfThoughtTrigger = ({  children,  className,  leftIcon,  swapIconOnHover = true,  ...props}: ChainOfThoughtTriggerProps) => (  <CollapsibleTrigger    className={cn(      "group text-muted-foreground hover:text-foreground flex cursor-pointer items-center justify-start gap-1 text-left text-sm transition-colors",      className,    )}    {...props}  >    <div className="flex items-center gap-2">      {leftIcon ? (        <span className="relative inline-flex size-4 items-center justify-center">          <span            className={cn(              "transition-opacity",              swapIconOnHover && "group-hover:opacity-0",            )}          >            {leftIcon}          </span>          {swapIconOnHover && (            <ChevronDown className="absolute size-4 opacity-0 transition-opacity group-hover:opacity-100 group-data-[state=open]:rotate-180" />          )}        </span>      ) : (        <span className="relative inline-flex size-4 items-center justify-center">          <Circle className="size-2 fill-current" />        </span>      )}      <span>{children}</span>    </div>    {!leftIcon && (      <ChevronDown className="size-4 transition-transform group-data-[state=open]:rotate-180" />    )}  </CollapsibleTrigger>);export type ChainOfThoughtContentProps = React.ComponentProps<  typeof CollapsibleContent>;export const ChainOfThoughtContent = ({  children,  className,  ...props}: ChainOfThoughtContentProps) => {  return (    <CollapsibleContent      className={cn(        "text-popover-foreground data-[state=closed]:animate-collapsible-up data-[state=open]:animate-collapsible-down overflow-hidden",        className,      )}      {...props}    >      <div className="grid grid-cols-[min-content_minmax(0,1fr)] gap-x-4">        <div className="bg-primary/20 ml-1.75 h-full w-px group-data-[last=true]:hidden" />        <div className="ml-1.75 h-full w-px bg-transparent group-data-[last=false]:hidden" />        <div className="mt-2 space-y-2">{children}</div>      </div>    </CollapsibleContent>  );};export type ChainOfThoughtProps = {  children: React.ReactNode;  className?: string;};export function ChainOfThought({ children, className }: ChainOfThoughtProps) {  const childrenArray = React.Children.toArray(children);  return (    <div className={cn("space-y-0", className)}>      {childrenArray.map((child, index) => (        <React.Fragment key={index}>          {React.isValidElement(child) &&            React.cloneElement(              child as React.ReactElement<ChainOfThoughtStepProps>,              {                isLast: index === childrenArray.length - 1,              },            )}        </React.Fragment>      ))}    </div>  );}export type ChainOfThoughtStepProps = {  children: React.ReactNode;  className?: string;  isLast?: boolean;};export const ChainOfThoughtStep = ({  children,  className,  isLast = false,  ...props}: ChainOfThoughtStepProps & React.ComponentProps<typeof Collapsible>) => {  return (    <Collapsible      className={cn("group", className)}      data-last={isLast}      {...props}    >      {children}      <div className="flex justify-start group-data-[last=true]:hidden">        <div className="bg-primary/20 ml-1.75 h-4 w-px" />      </div>    </Collapsible>  );};
Update the import paths to match your project setup.

Usage

import {
  ChainOfThought,
  ChainOfThoughtContent,
  ChainOfThoughtItem,
  ChainOfThoughtStep,
  ChainOfThoughtTrigger,
} from "@/components/ui/chain-of-thought";
<ChainOfThought>
  <ChainOfThoughtStep>
    <ChainOfThoughtTrigger>Analyzing the request</ChainOfThoughtTrigger>
    <ChainOfThoughtContent>
      <ChainOfThoughtItem>Breaking the task into steps.</ChainOfThoughtItem>
    </ChainOfThoughtContent>
  </ChainOfThoughtStep>
</ChainOfThought>

Composition

Use the following composition to build a ChainOfThought:

ChainOfThought
├── ChainOfThoughtStep
│   ├── ChainOfThoughtTrigger
│   └── ChainOfThoughtContent
│       └── ChainOfThoughtItem
└── ChainOfThoughtStep
    ├── ChainOfThoughtTrigger
    └── ChainOfThoughtContent
        └── ChainOfThoughtItem

ChainOfThought clones each ChainOfThoughtStep to mark the last one, so the connecting timeline stops at the final step. Each step is a Collapsible, so it accepts the usual open, defaultOpen, and onOpenChange props.

Examples

With icons

Pass a leftIcon to a ChainOfThoughtTrigger to replace the default bullet. By default the icon swaps to a chevron on hover; set swapIconOnHover={false} to keep the icon static.

Loading…
import {  ChainOfThought,  ChainOfThoughtContent,  ChainOfThoughtItem,  ChainOfThoughtStep,  ChainOfThoughtTrigger,} from "@/components/ui/chain-of-thought";import { BookOpen, Lightbulb, Search } from "lucide-react";export default function ChainOfThoughtIcons() {  return (    <ChainOfThought className="max-w-lg">      <ChainOfThoughtStep>        <ChainOfThoughtTrigger leftIcon={<Search className="size-4" />}>          Searching the knowledge base        </ChainOfThoughtTrigger>        <ChainOfThoughtContent>          <ChainOfThoughtItem>            Found 12 documents matching &quot;billing webhook retries&quot;.          </ChainOfThoughtItem>        </ChainOfThoughtContent>      </ChainOfThoughtStep>      <ChainOfThoughtStep>        <ChainOfThoughtTrigger leftIcon={<BookOpen className="size-4" />}>          Reading the top results        </ChainOfThoughtTrigger>        <ChainOfThoughtContent>          <ChainOfThoughtItem>            Retries use exponential backoff, capped at 5 attempts over 24 hours.          </ChainOfThoughtItem>        </ChainOfThoughtContent>      </ChainOfThoughtStep>      <ChainOfThoughtStep defaultOpen>        <ChainOfThoughtTrigger leftIcon={<Lightbulb className="size-4" />}>          Drawing a conclusion        </ChainOfThoughtTrigger>        <ChainOfThoughtContent>          <ChainOfThoughtItem>            The webhook likely failed silently after the last attempt. Enabling            the dead-letter queue should surface it.          </ChainOfThoughtItem>        </ChainOfThoughtContent>      </ChainOfThoughtStep>    </ChainOfThought>  );}

Open by default

Pass defaultOpen to a ChainOfThoughtStep to render it expanded, and add multiple ChainOfThoughtItem children to break a step into separate lines.

Loading…
import {  ChainOfThought,  ChainOfThoughtContent,  ChainOfThoughtItem,  ChainOfThoughtStep,  ChainOfThoughtTrigger,} from "@/components/ui/chain-of-thought";export default function ChainOfThoughtOpen() {  return (    <ChainOfThought className="max-w-lg">      <ChainOfThoughtStep defaultOpen>        <ChainOfThoughtTrigger>Planning the refactor</ChainOfThoughtTrigger>        <ChainOfThoughtContent>          <ChainOfThoughtItem>            Split the monolithic handler into smaller, testable units.          </ChainOfThoughtItem>          <ChainOfThoughtItem>            Extract the validation logic into a shared schema.          </ChainOfThoughtItem>          <ChainOfThoughtItem>            Add unit tests for each unit before wiring them together.          </ChainOfThoughtItem>        </ChainOfThoughtContent>      </ChainOfThoughtStep>      <ChainOfThoughtStep defaultOpen>        <ChainOfThoughtTrigger>Applying the changes</ChainOfThoughtTrigger>        <ChainOfThoughtContent>          <ChainOfThoughtItem>            Created schema.ts and moved the validation definitions over.          </ChainOfThoughtItem>          <ChainOfThoughtItem>            Updated 4 call sites to import from the new module.          </ChainOfThoughtItem>        </ChainOfThoughtContent>      </ChainOfThoughtStep>    </ChainOfThought>  );}

API Reference

ChainOfThought

PropTypeDescription
childrenReact.ReactNodeOne or more ChainOfThoughtStep items.
classNamestringAdditional classes for the wrapper.

ChainOfThoughtStep

Extends Collapsible (Radix Collapsible.Root), so it also accepts open, defaultOpen, and onOpenChange.

PropTypeDefaultDescription
childrenReact.ReactNodeA trigger and its content.
classNamestringAdditional classes for the step.
isLastbooleanfalseSet automatically by ChainOfThought.

ChainOfThoughtTrigger

Extends Radix Collapsible.Trigger.

PropTypeDefaultDescription
leftIconReact.ReactNodeIcon shown before the label instead of the bullet.
swapIconOnHoverbooleantrueSwap leftIcon for a chevron on hover.

ChainOfThoughtContent

Extends Radix Collapsible.Content. Renders the timeline rail and its children.

ChainOfThoughtItem

Extends div. A single line of reasoning inside a ChainOfThoughtContent.

On this page