Reasoning
Overview
The Reasoning component displays AI reasoning or thinking messages in a collapsible UI. Consecutive reasoning message parts are automatically grouped together with smooth animations and a shimmer effect while streaming.
Getting Started
Add reasoning
npx shadcn@latest add @assistant-ui/reasoningMain Component
npm install @assistant-ui/react tw-shimmeryarn add @assistant-ui/react tw-shimmerpnpm add @assistant-ui/react tw-shimmerbun add @assistant-ui/react tw-shimmerxpm add @assistant-ui/react tw-shimmer"use client";import { BrainIcon, ChevronDownIcon } from "lucide-react";import { memo, useCallback, useRef, useState, type FC, type PropsWithChildren,} from "react";import { useScrollLock, useAssistantState, type ReasoningMessagePartComponent, type ReasoningGroupComponent,} from "@assistant-ui/react";import { MarkdownText } from "@/components/assistant-ui/markdown-text";import { Collapsible, CollapsibleContent, CollapsibleTrigger,} from "@/components/ui/collapsible";import { cn } from "@/lib/utils";const ANIMATION_DURATION = 200;/** * Root collapsible container that manages open/closed state and scroll lock. * Provides animation timing via CSS variable and prevents scroll jumps on collapse. */const ReasoningRoot: FC< PropsWithChildren<{ className?: string; }>> = ({ className, children }) => { const collapsibleRef = useRef<HTMLDivElement>(null); const [isOpen, setIsOpen] = useState(false); const lockScroll = useScrollLock(collapsibleRef, ANIMATION_DURATION); const handleOpenChange = useCallback( (open: boolean) => { if (!open) { lockScroll(); } setIsOpen(open); }, [lockScroll], ); return ( <Collapsible ref={collapsibleRef} open={isOpen} onOpenChange={handleOpenChange} className={cn("aui-reasoning-root mb-4 w-full", className)} style={ { "--animation-duration": `${ANIMATION_DURATION}ms`, } as React.CSSProperties } > {children} </Collapsible> );};ReasoningRoot.displayName = "ReasoningRoot";/** * Gradient overlay that softens the bottom edge during expand/collapse animations. * Animation: Fades out with delay when opening and fades back in when closing. */const GradientFade: FC<{ className?: string }> = ({ className }) => ( <div className={cn( "aui-reasoning-fade pointer-events-none absolute inset-x-0 bottom-0 z-10 h-16", "bg-[linear-gradient(to_top,var(--color-background),transparent)]", "fade-in-0 animate-in", "group-data-[state=open]/collapsible-content:animate-out", "group-data-[state=open]/collapsible-content:fade-out-0", "group-data-[state=open]/collapsible-content:delay-[calc(var(--animation-duration)*0.75)]", // calc for timing the delay "group-data-[state=open]/collapsible-content:fill-mode-forwards", "duration-(--animation-duration)", "group-data-[state=open]/collapsible-content:duration-(--animation-duration)", className, )} />);/** * Trigger button for the Reasoning collapsible. * Composed of icons, label, and text shimmer animation when reasoning is being streamed. */const ReasoningTrigger: FC<{ active: boolean; className?: string }> = ({ active, className,}) => ( <CollapsibleTrigger className={cn( "aui-reasoning-trigger group/trigger -mb-2 flex max-w-[75%] items-center gap-2 py-2 text-muted-foreground text-sm transition-colors hover:text-foreground", className, )} > <BrainIcon className="aui-reasoning-trigger-icon size-4 shrink-0" /> <span className="aui-reasoning-trigger-label-wrapper relative inline-block leading-none"> <span>Reasoning</span> {active ? ( <span aria-hidden className="aui-reasoning-trigger-shimmer shimmer pointer-events-none absolute inset-0 motion-reduce:animate-none" > Reasoning </span> ) : null} </span> <ChevronDownIcon className={cn( "aui-reasoning-trigger-chevron mt-0.5 size-4 shrink-0", "transition-transform duration-(--animation-duration) ease-out", "group-data-[state=closed]/trigger:-rotate-90", "group-data-[state=open]/trigger:rotate-0", )} /> </CollapsibleTrigger>);/** * Collapsible content wrapper that handles height expand/collapse animation. * Animation: Height animates up (collapse) and down (expand). * Also provides group context for child animations via data-state attributes. */const ReasoningContent: FC< PropsWithChildren<{ className?: string; "aria-busy"?: boolean; }>> = ({ className, children, "aria-busy": ariaBusy }) => ( <CollapsibleContent className={cn( "aui-reasoning-content relative overflow-hidden text-muted-foreground text-sm outline-none", "group/collapsible-content ease-out", "data-[state=closed]:animate-collapsible-up", "data-[state=open]:animate-collapsible-down", "data-[state=closed]:fill-mode-forwards", "data-[state=closed]:pointer-events-none", "data-[state=open]:duration-(--animation-duration)", "data-[state=closed]:duration-(--animation-duration)", className, )} aria-busy={ariaBusy} > {children} <GradientFade /> </CollapsibleContent>);ReasoningContent.displayName = "ReasoningContent";/** * Text content wrapper that animates the reasoning text visibility. * Animation: Slides in from top + fades in when opening, reverses when closing. * Reacts to parent ReasoningContent's data-state via Radix group selectors. */const ReasoningText: FC< PropsWithChildren<{ className?: string; }>> = ({ className, children }) => ( <div className={cn( "aui-reasoning-text relative z-0 space-y-4 pt-4 pl-6 leading-relaxed", "transform-gpu transition-[transform,opacity]", "group-data-[state=open]/collapsible-content:animate-in", "group-data-[state=closed]/collapsible-content:animate-out", "group-data-[state=open]/collapsible-content:fade-in-0", "group-data-[state=closed]/collapsible-content:fade-out-0", "group-data-[state=open]/collapsible-content:slide-in-from-top-4", "group-data-[state=closed]/collapsible-content:slide-out-to-top-4", "group-data-[state=open]/collapsible-content:duration-(--animation-duration)", "group-data-[state=closed]/collapsible-content:duration-(--animation-duration)", "[&_p]:-mb-2", className, )} > {children} </div>);ReasoningText.displayName = "ReasoningText";/** * Renders a single reasoning part's text with markdown support. * Consecutive reasoning parts are automatically grouped by ReasoningGroup. * * Pass Reasoning to MessagePrimitive.Parts in thread.tsx * * @example: * ```tsx * <MessagePrimitive.Parts * components={{ * Reasoning: Reasoning, * ReasoningGroup: ReasoningGroup, * }} * /> * ``` */const ReasoningImpl: ReasoningMessagePartComponent = () => <MarkdownText />;/** * Collapsible wrapper that groups consecutive reasoning parts together. * Includes scroll lock to prevent page jumps during collapse animation. * * Pass ReasoningGroup to MessagePrimitive.Parts in thread.tsx * * @example: * ```tsx * <MessagePrimitive.Parts * components={{ * Reasoning: Reasoning, * ReasoningGroup: ReasoningGroup, * }} * /> * ``` */const ReasoningGroupImpl: ReasoningGroupComponent = ({ children, startIndex, endIndex,}) => { /** * Detects if reasoning is currently streaming within this group's range. */ const isReasoningStreaming = useAssistantState(({ message }) => { if (message.status?.type !== "running") return false; const lastIndex = message.parts.length - 1; if (lastIndex < 0) return false; const lastType = message.parts[lastIndex]?.type; if (lastType !== "reasoning") return false; return lastIndex >= startIndex && lastIndex <= endIndex; }); return ( <ReasoningRoot> <ReasoningTrigger active={isReasoningStreaming} /> <ReasoningContent aria-busy={isReasoningStreaming}> <ReasoningText>{children}</ReasoningText> </ReasoningContent> </ReasoningRoot> );};export const Reasoning = memo(ReasoningImpl);Reasoning.displayName = "Reasoning";export const ReasoningGroup = memo(ReasoningGroupImpl);ReasoningGroup.displayName = "ReasoningGroup";assistant-ui dependencies
npm install @assistant-ui/react-markdown remark-gfmyarn add @assistant-ui/react-markdown remark-gfmpnpm add @assistant-ui/react-markdown remark-gfmbun add @assistant-ui/react-markdown remark-gfmxpm add @assistant-ui/react-markdown remark-gfm"use client";import "@assistant-ui/react-markdown/styles/dot.css";import { type CodeHeaderProps, MarkdownTextPrimitive, unstable_memoizeMarkdownComponents as memoizeMarkdownComponents, useIsMarkdownCodeBlock,} from "@assistant-ui/react-markdown";import remarkGfm from "remark-gfm";import { type FC, memo, useState } from "react";import { CheckIcon, CopyIcon } from "lucide-react";import { TooltipIconButton } from "@/components/assistant-ui/tooltip-icon-button";import { cn } from "@/lib/utils";const MarkdownTextImpl = () => { return ( <MarkdownTextPrimitive remarkPlugins={[remarkGfm]} className="aui-md" components={defaultComponents} /> );};export const MarkdownText = memo(MarkdownTextImpl);const CodeHeader: FC<CodeHeaderProps> = ({ language, code }) => { const { isCopied, copyToClipboard } = useCopyToClipboard(); const onCopy = () => { if (!code || isCopied) return; copyToClipboard(code); }; return ( <div className="aui-code-header-root mt-4 flex items-center justify-between gap-4 rounded-t-lg bg-muted-foreground/15 px-4 py-2 font-semibold text-foreground text-sm dark:bg-muted-foreground/20"> <span className="aui-code-header-language lowercase [&>span]:text-xs"> {language} </span> <TooltipIconButton tooltip="Copy" onClick={onCopy}> {!isCopied && <CopyIcon />} {isCopied && <CheckIcon />} </TooltipIconButton> </div> );};const useCopyToClipboard = ({ copiedDuration = 3000,}: { copiedDuration?: number;} = {}) => { const [isCopied, setIsCopied] = useState<boolean>(false); const copyToClipboard = (value: string) => { if (!value) return; navigator.clipboard.writeText(value).then(() => { setIsCopied(true); setTimeout(() => setIsCopied(false), copiedDuration); }); }; return { isCopied, copyToClipboard };};const defaultComponents = memoizeMarkdownComponents({ h1: ({ className, ...props }) => ( <h1 className={cn( "aui-md-h1 mb-8 scroll-m-20 font-extrabold text-4xl tracking-tight last:mb-0", className, )} {...props} /> ), h2: ({ className, ...props }) => ( <h2 className={cn( "aui-md-h2 mt-8 mb-4 scroll-m-20 font-semibold text-3xl tracking-tight first:mt-0 last:mb-0", className, )} {...props} /> ), h3: ({ className, ...props }) => ( <h3 className={cn( "aui-md-h3 mt-6 mb-4 scroll-m-20 font-semibold text-2xl tracking-tight first:mt-0 last:mb-0", className, )} {...props} /> ), h4: ({ className, ...props }) => ( <h4 className={cn( "aui-md-h4 mt-6 mb-4 scroll-m-20 font-semibold text-xl tracking-tight first:mt-0 last:mb-0", className, )} {...props} /> ), h5: ({ className, ...props }) => ( <h5 className={cn( "aui-md-h5 my-4 font-semibold text-lg first:mt-0 last:mb-0", className, )} {...props} /> ), h6: ({ className, ...props }) => ( <h6 className={cn( "aui-md-h6 my-4 font-semibold first:mt-0 last:mb-0", className, )} {...props} /> ), p: ({ className, ...props }) => ( <p className={cn( "aui-md-p mt-5 mb-5 leading-7 first:mt-0 last:mb-0", className, )} {...props} /> ), a: ({ className, ...props }) => ( <a className={cn( "aui-md-a font-medium text-primary underline underline-offset-4", className, )} {...props} /> ), blockquote: ({ className, ...props }) => ( <blockquote className={cn("aui-md-blockquote border-l-2 pl-6 italic", className)} {...props} /> ), ul: ({ className, ...props }) => ( <ul className={cn("aui-md-ul my-5 ml-6 list-disc [&>li]:mt-2", className)} {...props} /> ), ol: ({ className, ...props }) => ( <ol className={cn("aui-md-ol my-5 ml-6 list-decimal [&>li]:mt-2", className)} {...props} /> ), hr: ({ className, ...props }) => ( <hr className={cn("aui-md-hr my-5 border-b", className)} {...props} /> ), table: ({ className, ...props }) => ( <table className={cn( "aui-md-table my-5 w-full border-separate border-spacing-0 overflow-y-auto", className, )} {...props} /> ), th: ({ className, ...props }) => ( <th className={cn( "aui-md-th bg-muted px-4 py-2 text-left font-bold first:rounded-tl-lg last:rounded-tr-lg [[align=center]]:text-center [[align=right]]:text-right", className, )} {...props} /> ), td: ({ className, ...props }) => ( <td className={cn( "aui-md-td border-b border-l px-4 py-2 text-left last:border-r [[align=center]]:text-center [[align=right]]:text-right", className, )} {...props} /> ), tr: ({ className, ...props }) => ( <tr className={cn( "aui-md-tr m-0 border-b p-0 first:border-t [&:last-child>td:first-child]:rounded-bl-lg [&:last-child>td:last-child]:rounded-br-lg", className, )} {...props} /> ), sup: ({ className, ...props }) => ( <sup className={cn("aui-md-sup [&>a]:text-xs [&>a]:no-underline", className)} {...props} /> ), pre: ({ className, ...props }) => ( <pre className={cn( "aui-md-pre overflow-x-auto rounded-t-none! rounded-b-lg bg-black p-4 text-white", className, )} {...props} /> ), code: function Code({ className, ...props }) { const isCodeBlock = useIsMarkdownCodeBlock(); return ( <code className={cn( !isCodeBlock && "aui-md-inline-code rounded border bg-muted font-semibold", className, )} {...props} /> ); }, CodeHeader,});"use client";import { ComponentPropsWithRef, forwardRef } from "react";import { Slottable } from "@radix-ui/react-slot";import { Tooltip, TooltipContent, TooltipTrigger,} from "@/components/ui/tooltip";import { Button } from "@/components/ui/button";import { cn } from "@/lib/utils";export type TooltipIconButtonProps = ComponentPropsWithRef<typeof Button> & { tooltip: string; side?: "top" | "bottom" | "left" | "right";};export const TooltipIconButton = forwardRef< HTMLButtonElement, TooltipIconButtonProps>(({ children, tooltip, side = "bottom", className, ...rest }, ref) => { return ( <Tooltip> <TooltipTrigger asChild> <Button variant="ghost" size="icon" {...rest} className={cn("aui-button-icon size-6 p-1", className)} ref={ref} > <Slottable>{children}</Slottable> <span className="aui-sr-only sr-only">{tooltip}</span> </Button> </TooltipTrigger> <TooltipContent side={side}>{tooltip}</TooltipContent> </Tooltip> );});TooltipIconButton.displayName = "TooltipIconButton";shadcn/ui dependencies
npm install @radix-ui/react-collapsible @radix-ui/react-slot @radix-ui/react-tooltipyarn add @radix-ui/react-collapsible @radix-ui/react-slot @radix-ui/react-tooltippnpm add @radix-ui/react-collapsible @radix-ui/react-slot @radix-ui/react-tooltipbun add @radix-ui/react-collapsible @radix-ui/react-slot @radix-ui/react-tooltipxpm add @radix-ui/react-collapsible @radix-ui/react-slot @radix-ui/react-tooltip"use client";import * as CollapsiblePrimitive from "@radix-ui/react-collapsible";function Collapsible({ ...props}: React.ComponentProps<typeof CollapsiblePrimitive.Root>) { return <CollapsiblePrimitive.Root data-slot="collapsible" {...props} />;}function CollapsibleTrigger({ ...props}: React.ComponentProps<typeof CollapsiblePrimitive.CollapsibleTrigger>) { return ( <CollapsiblePrimitive.CollapsibleTrigger data-slot="collapsible-trigger" {...props} /> );}function CollapsibleContent({ ...props}: React.ComponentProps<typeof CollapsiblePrimitive.CollapsibleContent>) { return ( <CollapsiblePrimitive.CollapsibleContent data-slot="collapsible-content" {...props} /> );}export { Collapsible, CollapsibleTrigger, CollapsibleContent };"use client";import * as React from "react";import * as TooltipPrimitive from "@radix-ui/react-tooltip";import { cn } from "@/lib/utils";function TooltipProvider({ delayDuration = 0, ...props}: React.ComponentProps<typeof TooltipPrimitive.Provider>) { return ( <TooltipPrimitive.Provider data-slot="tooltip-provider" delayDuration={delayDuration} {...props} /> );}function Tooltip({ ...props}: React.ComponentProps<typeof TooltipPrimitive.Root>) { return ( <TooltipProvider> <TooltipPrimitive.Root data-slot="tooltip" {...props} /> </TooltipProvider> );}function TooltipTrigger({ ...props}: React.ComponentProps<typeof TooltipPrimitive.Trigger>) { return <TooltipPrimitive.Trigger data-slot="tooltip-trigger" {...props} />;}function TooltipContent({ className, sideOffset = 0, children, ...props}: React.ComponentProps<typeof TooltipPrimitive.Content>) { return ( <TooltipPrimitive.Portal> <TooltipPrimitive.Content data-slot="tooltip-content" sideOffset={sideOffset} className={cn( "fade-in-0 zoom-in-95 data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-fit origin-(--radix-tooltip-content-transform-origin) animate-in text-balance rounded-md bg-foreground px-3 py-1.5 text-background text-xs data-[state=closed]:animate-out", className, )} {...props} > {children} <TooltipPrimitive.Arrow className="z-50 size-2.5 translate-y-[calc(-50%_-_2px)] rotate-45 rounded-[2px] bg-foreground fill-foreground" /> </TooltipPrimitive.Content> </TooltipPrimitive.Portal> );}export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider };import * as React from "react";import { Slot } from "@radix-ui/react-slot";import { cva, type VariantProps } from "class-variance-authority";import { cn } from "@/lib/utils";const buttonVariants = cva( "inline-flex shrink-0 items-center justify-center gap-2 whitespace-nowrap rounded-md font-medium text-sm outline-none transition-all focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0", { variants: { variant: { default: "bg-primary text-primary-foreground hover:bg-primary/90", destructive: "bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:bg-destructive/60 dark:focus-visible:ring-destructive/40", outline: "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50", secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80", ghost: "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50", link: "text-primary underline-offset-4 hover:underline", }, size: { default: "h-9 px-4 py-2 has-[>svg]:px-3", sm: "h-8 gap-1.5 rounded-md px-3 has-[>svg]:px-2.5", lg: "h-10 rounded-md px-6 has-[>svg]:px-4", icon: "size-9", "icon-sm": "size-8", "icon-lg": "size-10", }, }, defaultVariants: { variant: "default", size: "default", }, },);function Button({ className, variant = "default", size = "default", asChild = false, ...props}: React.ComponentProps<"button"> & VariantProps<typeof buttonVariants> & { asChild?: boolean; }) { const Comp = asChild ? Slot : "button"; return ( <Comp data-slot="button" data-variant={variant} data-size={size} className={cn(buttonVariants({ variant, size, className }))} {...props} /> );}export { Button, buttonVariants };This adds a /components/assistant-ui/reasoning.tsx file to your project, which you can adjust as needed.
Use in your application
Pass the Reasoning and ReasoningGroup components to the MessagePrimitive.Parts component:
import { MessagePrimitive } from "@assistant-ui/react";
import { Reasoning, ReasoningGroup } from "@/components/assistant-ui/reasoning";
const AssistantMessage: FC = () => {
return (
<MessagePrimitive.Root className="...">
<div className="...">
<MessagePrimitive.Parts
components={{
Reasoning: Reasoning,
ReasoningGroup: ReasoningGroup
}}
/>
</div>
<AssistantActionBar />
<BranchPicker className="..." />
</MessagePrimitive.Root>
);
};How It Works
The component consists of two parts:
- Reasoning: Renders individual reasoning message part content
- ReasoningGroup: Wraps consecutive reasoning parts in a collapsible container
Consecutive reasoning parts are automatically grouped together by the ReasoningGroup component, similar to how ToolGroup handles tool calls.
Reasoning
The Reasoning component doesn't accept additional props—it renders the reasoning text content with markdown support.
Examples
Basic Usage
<MessagePrimitive.Parts
components={{
Reasoning,
ReasoningGroup
}}
/>Custom Styling
Since the component is copied to your project, you can customize it directly by modifying the reasoning.tsx file. The internal components (ReasoningRoot, ReasoningTrigger, ReasoningContent, ReasoningText) accept className props for styling:
const ReasoningGroupImpl: ReasoningGroupComponent = ({
// ... existing code ...
return (
<ReasoningRoot className="rounded-lg border bg-muted/50 p-4">
<ReasoningTrigger
active={isReasoningStreaming}
className="font-semibold text-foreground"
/>
<ReasoningContent
aria-busy={isReasoningStreaming}
className="mt-2"
>
<ReasoningText className="text-base">{children}</ReasoningText>
</ReasoningContent>
</ReasoningRoot>
);
};You can also customize the individual internal components:
const ReasoningRoot: FC<PropsWithChildren<{ className?: string }>> = ({
// ... existing code ...
return (
<Collapsible
// ...
className={cn("aui-reasoning-root mb-4 w-full rounded-lg border bg-muted/50 p-4", className)}
// ...
>
{children}
</Collapsible>
);
};
const ReasoningTrigger: FC<{ active: boolean; className?: string }> = ({
// ... existing code ...
<CollapsibleTrigger
className={cn(
"aui-reasoning-trigger group/trigger -mb-2 flex max-w-[75%] items-center gap-2 py-2 text-sm font-semibold text-foreground transition-colors hover:text-foreground",
className,
)}
>
{/* ... existing content ... */}
</CollapsibleTrigger>
);Technical Details
Scroll Lock
The component uses the useScrollLock hook (exported from @assistant-ui/react) to prevent page jumps when collapsing the reasoning section. This maintains the scroll position during the collapse animation.
Animation Timing
The component uses CSS custom properties for animation timing:
--animation-duration: Controls expand/collapse animation (default: 200ms)--shimmer-duration: Controls the shimmer effect speed (default: 1000ms)
These can be customized by modifying the CSS variables in your component.
Related Components
- ToolGroup - Similar grouping pattern for tool calls
- PartGrouping - Experimental API for grouping message parts