Timace

Reasoning

Read the pricing page
Checked what converts
Chose what to cut
Install
npm install react-aria-components
components/ui/reasoning.tsx
"use client";

import type { ReactNode } from "react";
import { Button, Disclosure, DisclosurePanel, type DisclosureProps } from "react-aria-components";

const styles = {
  root: "group flex w-full flex-col font-(family-name:--tui-font) text-(length:--tui-reasoning-font-size)",
  trigger:
    "inline-flex cursor-default items-center gap-1.5 self-start rounded-(--tui-reasoning-radius) border-none bg-transparent p-0 font-(family-name:--tui-font) text-(length:--tui-reasoning-font-size) leading-none font-(--tui-reasoning-font-weight) text-(--tui-reasoning-fg) outline-none transition-colors duration-(--tui-duration) data-focus-visible:[outline:var(--tui-reasoning-ring-width)_solid_var(--tui-reasoning-ring)] data-focus-visible:[outline-offset:var(--tui-reasoning-ring-offset)] data-hovered:text-(--tui-reasoning-fg-hover)",
  mark: "size-[1.15em] shrink-0",
  duration: "font-normal text-(--tui-reasoning-body) tabular-nums",
  chevron: "size-3.5 shrink-0 transition-transform duration-(--tui-duration) group-data-expanded:rotate-90",
  shimmer:
    "bg-[linear-gradient(90deg,var(--tui-reasoning-fg)_35%,var(--tui-reasoning-fg-hover)_50%,var(--tui-reasoning-fg)_65%)] bg-[length:250%_100%] bg-clip-text text-transparent [animation:tui-shimmer_2s_linear_infinite] motion-reduce:animate-none motion-reduce:text-(--tui-reasoning-fg)",
  panel: "mt-(--tui-reasoning-gap) flex flex-col gap-(--tui-reasoning-step-gap) border-solid border-(--tui-reasoning-bar) border-l-(length:--tui-reasoning-bar-width) pl-(--tui-reasoning-padding) leading-relaxed text-(--tui-reasoning-body)",
  step: "relative flex flex-col gap-0.5 before:absolute before:top-[0.55em] before:left-[calc((var(--tui-reasoning-padding)_+_var(--tui-reasoning-bar-width)_/_2_+_3px)_*_-1)] before:size-1.5 before:rounded-full before:bg-(--tui-reasoning-dot)",
  stepTitle: "font-medium text-(--tui-reasoning-fg-hover)",
};

export interface ReasoningProps extends Omit<DisclosureProps, "className" | "children"> {
  /** The line that is always shown, e.g. "Thought" or "Searched the web". */
  summary: ReactNode;
  /** How long it took, e.g. "4s". */
  duration?: ReactNode;
  /** Still going: the summary shimmers. */
  isThinking?: boolean;
  /** The working. Plain text, or ReasoningStep for each stage. */
  children: ReactNode;
  className?: string;
}

/** The working behind a reply, folded away under one line. */
export function Reasoning({ summary, duration, isThinking, children, className, ...props }: ReasoningProps) {
  return (
    <Disclosure {...props} className={[styles.root, className].filter(Boolean).join(" ")}>
      <Button slot="trigger" className={styles.trigger}>
        <svg aria-hidden viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.4" strokeLinecap="round" strokeLinejoin="round" className={`${styles.mark} ${isThinking ? "animate-pulse" : ""}`}>
          <path d="M8 2.5a3.7 3.7 0 0 0-2.2 6.700c.4.300.700.800.700 1.300V11h3v-.500c0-.500.300-1 .700-1.300A3.700 3.700 0 0 0 8 2.500zM6.500 13.500h3" />
        </svg>
        <span className={isThinking ? styles.shimmer : undefined}>{summary}</span>
        {duration && <span className={styles.duration}>{duration}</span>}
        <svg aria-hidden viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.75" strokeLinecap="round" strokeLinejoin="round" className={styles.chevron}>
          <path d="M6 4l4 4-4 4" />
        </svg>
      </Button>
      <DisclosurePanel>
        <div data-slot="reasoning" className={styles.panel}>
          {children}
        </div>
      </DisclosurePanel>
    </Disclosure>
  );
}

/** One stage of the working, marked on the rail. */
export function ReasoningStep({ title, children }: { title: ReactNode; children?: ReactNode }) {
  return (
    <div className={styles.step}>
      <span className={styles.stepTitle}>{title}</span>
      {children && <span>{children}</span>}
    </div>
  );
}
styles/timace-ui.css
:root {
  color-scheme: light;
  --tui-bg: #ffffff;
  --tui-fg: #0a0a0a;
  --tui-muted: #f5f5f5;
  --tui-muted-fg: #737373;
  --tui-border: #e5e5e5;
  --tui-primary: #171717;
  --tui-primary-fg: #fafafa;
  --tui-ring: #a3a3a3;
  --tui-success: #16a34a;
  --tui-warning: #d97706;
  --tui-danger: #dc2626;
  --tui-radius: 8px;
  --tui-border-width: 1px;
  --tui-space: 4px;
  --tui-shadow: none;
  --tui-font: ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
  --tui-text: 14px;
  --tui-duration: 150ms;

  /* Reasoning */
  --tui-reasoning-gap: calc(var(--tui-space) * 3);
  --tui-reasoning-step-gap: calc(var(--tui-space) * 3);
  --tui-reasoning-padding: calc(var(--tui-space) * 4);
  --tui-reasoning-bar-width: 2px;
  --tui-reasoning-radius: calc(var(--tui-radius) * 0.5);
  --tui-reasoning-font-size: var(--tui-text);
  --tui-reasoning-font-weight: 500;
  --tui-reasoning-fg: var(--tui-muted-fg);
  --tui-reasoning-body: var(--tui-muted-fg);
  --tui-reasoning-bar: var(--tui-border);
  --tui-reasoning-dot: color-mix(in oklab, var(--tui-muted-fg) 60%, var(--tui-bg));
  --tui-reasoning-fg-hover: var(--tui-fg);
  --tui-reasoning-ring: var(--tui-ring);
  --tui-reasoning-ring-width: 2px;
  --tui-reasoning-ring-offset: 2px;
}

.dark,
[data-theme="dark"] {
  color-scheme: dark;
  --tui-bg: #0a0a0a;
  --tui-fg: #fafafa;
  --tui-muted: #1c1c1c;
  --tui-muted-fg: #a3a3a3;
  --tui-border: #2a2a2a;
  --tui-primary: #fafafa;
  --tui-primary-fg: #171717;
  --tui-ring: #737373;
  --tui-success: #22c55e;
  --tui-warning: #f59e0b;
  --tui-danger: #ef4444;
}

@keyframes tui-shimmer {
  from { background-position: 125% 0; }
  to { background-position: -125% 0; }
}

Needs Tailwind CSS v4. Import the stylesheet once, after Tailwind.