Chain of Thought
Display step-by-step reasoning and collapsible thought processes in AI interfaces.
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-thoughtnpm install radix-ui lucide-reactcollapsible component that Chain of Thought builds on:npx shadcn@latest add @cortexcn/collapsible@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;
}
}
}"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> );};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
└── ChainOfThoughtItemChainOfThought 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.
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 "billing webhook retries". </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.
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
| Prop | Type | Description |
|---|---|---|
children | React.ReactNode | One or more ChainOfThoughtStep items. |
className | string | Additional classes for the wrapper. |
ChainOfThoughtStep
Extends Collapsible (Radix Collapsible.Root), so it also accepts open,
defaultOpen, and onOpenChange.
| Prop | Type | Default | Description |
|---|---|---|---|
children | React.ReactNode | A trigger and its content. | |
className | string | Additional classes for the step. | |
isLast | boolean | false | Set automatically by ChainOfThought. |
ChainOfThoughtTrigger
Extends Radix Collapsible.Trigger.
| Prop | Type | Default | Description |
|---|---|---|---|
leftIcon | React.ReactNode | Icon shown before the label instead of the bullet. | |
swapIconOnHover | boolean | true | Swap 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.