HextaUI

Thinking

The thinking view of an AI chat. A live label follows each step, searches and sources fold into a trail, and citations sit beside the answer.

Open in new tab

Long waits feel shorter when you can see the work happening. Thinking puts a live status line at the top of an answer: a small shader orb, the current step in plain words such as “Searching the web”, and a running timer. Open it to read the full trail of searches, sources and reasoning.

Reasoning written as a bold heading followed by a paragraph splits into sections, and half-typed headings are held back until they close. Tool steps show a spinner and a present-tense label while they run, then switch to what they did, with source chips underneath.

The trail opens while the model works and collapses to “Thought for 12s” when it finishes, unless someone has opened or closed it themselves. The answer then cites its sources inline: CitedText turns [1] and [2][3] into compact chips with the title, domain and snippet on hover.

  1. Get HextaUI Pro

    One payment unlocks every block, including future ones. See pricing.

  2. Add the Pro registry to components.json

    components.json
    {
      "registries": {
        "@hextaui-pro": {
          "url": "https://hextaui.com/r/pro/{name}.json",
          "headers": {
            "Authorization": "Bearer ${HEXTAUI_PRO_TOKEN}"
          }
        }
      }
    }
  3. Add your token

    Create a token on your account page and put it in .env.local as HEXTAUI_PRO_TOKEN.

  4. Add the block

    pnpm dlx shadcn@latest add @hextaui-pro/thinking

With the AI SDK

Map a useChat message’s parts: reasoning parts into Reasoning and text into CitedText, with source-url parts as its sources. Ask the model to cite with [1], [2] at the end of the sentence they support.

"use client"

import { useChat } from "@ai-sdk/react"
import type { UIMessage } from "ai"

import { CitedText } from "@/components/blocks/thinking/cited-text"
import { Reasoning } from "@/components/blocks/thinking/reasoning"
import type { Source } from "@/components/blocks/thinking/sources"

function AssistantMessage({ message }: { message: UIMessage }) {
  const sources: Source[] = message.parts.flatMap((part) =>
    part.type === "source-url"
      ? [{ id: part.sourceId, url: part.url, title: part.title ?? part.url }]
      : []
  )

  return (
    <div className="flex flex-col gap-4">
      {message.parts.map((part, index) => {
        if (part.type === "reasoning") {
          return (
            <Reasoning
              key={index}
              text={part.text}
              streaming={part.state === "streaming"}
            />
          )
        }
        if (part.type === "text") {
          return (
            <CitedText
              key={index}
              text={part.text}
              sources={sources}
              streaming={part.state === "streaming"}
            />
          )
        }
        return null
      })}
    </div>
  )
}

export function Chat() {
  const { messages } = useChat()

  return messages.map((message) =>
    message.role === "assistant" ? (
      <AssistantMessage key={message.id} message={message} />
    ) : null
  )
}

With tool steps

Turn tool calls into steps. A call that is still running shows a spinner and a shimmer, and checks off when its output arrives.

"use client"

import { getToolName, isToolUIPart, type UIMessage } from "ai"

import { Reasoning, type ReasoningStep } from "@/components/blocks/thinking/reasoning"

const labels: Record<string, Pick<ReasoningStep, "label" | "kind">> = {
  webSearch: { label: "Searching the web", kind: "search" },
  readPage: { label: "Reading sources", kind: "read" },
  runCode: { label: "Running code", kind: "code" },
}

export function Thoughts({
  message,
  streaming,
}: {
  message: UIMessage
  streaming: boolean
}) {
  const steps: ReasoningStep[] = message.parts
    .filter(isToolUIPart)
    .map((part) => {
      const name = getToolName(part)
      return {
        id: part.toolCallId,
        ...(labels[name] ?? { label: name, kind: "think" }),
        status: part.state.startsWith("output") ? "done" : "active",
      }
    })

  const text = message.parts
    .flatMap((part) => (part.type === "reasoning" ? [part.text] : []))
    .join("\n\n")

  return <Reasoning steps={steps} text={text} streaming={streaming} />
}

Anatomy

The parts you compose, from the outside in.

PartDescription
ReasoningThe collapsible status line and trail. Takes reasoning text, steps, or both.
ThinkingOrbThe animated shader orb. Use it alone as a compact busy indicator.
CitedTextThe answer, with [n] marks turned into citation chips.
CitationOne chip with a hover card listing its sources.
SourceChipsA row of source chips, used under a step.

Reasoning

Also accepts every div prop except children.

PropTypeDefault
textReasoning so far. **Bold** lines become section headings.
string""
streamingWhether the model is still working. Drives the orb, the shimmer and the timer.
booleanfalse
stepsTool steps shown above the reasoning.
ReasoningStep[][]
durationSeconds to show once finished. Leave it out to use the measured time.
number–
labelOverrides the live label while streaming.
string–
defaultOpenInitial open state. Defaults to open while streaming.
boolean–
autoCollapseCollapse when streaming ends, unless someone toggled it.
booleantrue

ReasoningStep

One entry in steps.

PropTypeDefault
idStable key.
string–
labelWhat it is doing, for example “Searching for debounce”.
string–
doneLabelWhat it did, shown once status is done.
string–
statusActive steps show a spinner and drive the live label.
"active" | "done"–
kindPicks the icon and the phase text.
"search" | "read" | "think" | "code""think"
phaseCustom phase text while the trail is open.
string–
detailExtra content under the step.
ReactNode–
sourcesChips shown under the step.
Source[]–

CitedText

Also accepts every div prop except children.

PropTypeDefault
textThe answer, with [1]-style marks after the claims.
string–
sourcesSources in citation order, so [1] is sources[0].
Source[]–
streamingShows the caret and paces the text.
booleanfalse
smoothTurn pacing off.
booleantrue
PropTypeDefault
idStable key.
string–
urlOpened in a new tab.
string–
titleShown in the hover card.
string–
nameShort site name for the chip. Defaults to the domain.
string–
snippetThe quote shown under the title.
string–
iconFavicon URL. Falls back to the first letter.
string–

ThinkingOrb

Also accepts every span prop.

PropTypeDefault
activeAnimates while true and rests when false.
boolean–
colorsThree hex colors for the shader.
{ deep, mid, light }blue
speedAnimation speed multiplier.
number–
KeyAction
TabMoves to the status line, then into the trail, then to each citation. A focused citation opens its hover card.
EnterSpaceOpens or closes the trail.
EscCloses an open citation card.
  • The status line is a button that announces its summary through a polite live region, so screen reader users hear “Searching the web”, then “Thought for 12s”, without every timer tick.
  • Each citation chip is a real link labelled “Source: MDN: Debounce”, so it reads as a source, not a number.
  • With reduced motion the orb renders a still frame, the shimmer stops and rows appear without sliding.

Built with

The free HextaUI components Thinking is made from. Each one installs on its own.

Code

6 files, added to components/blocks/thinking.