Elements

Elements · Structured output

Math

Rendered expressions with the working shown, one step at a time.

derivation
p(x) = 11 + e−xthe logistic
fig. 01 · plays once, replay from the corner

Installation

npx shadcn@latest add "@assistant-ui/elements-math-block"
First time? Set up a runtime

Runtime components read their state from an assistant-ui runtime. Add one to an existing project:

npx assistant-ui@latest init

Then wrap your app in a runtime provider:

import { AssistantRuntimeProvider } from "@assistant-ui/react";
import { useChatRuntime, AssistantChatTransport } from "@assistant-ui/ai-sdk";

export default function App() {
  const runtime = useChatRuntime({
    transport: new AssistantChatTransport({ api: "/api/chat" }),
  });

  return (
    <AssistantRuntimeProvider runtime={runtime}>
      {/* your components */}
    </AssistantRuntimeProvider>
  );
}

The installation guide covers new projects, templates, and API routes.

An answer shown as its working rather than its final result: a short list of steps, each an expression with an optional note underneath, revealed only as far as you say. With a runtime the steps arrive from a tool call as the model streams them; standalone you hold the step array and the reveal count yourself.

Getting started

This element has no assistant-ui primitive of its own: a derivation is exactly the kind of structured content a tool call carries, so the runtime wiring is a tool renderer rather than a primitive composition.

Register the render function

app/toolkit.tsx
"use client";

import { defineToolkit } from "@assistant-ui/react";
import { MathBlock } from "@/components/assistant-ui/elements/math-block";

export const toolkit = defineToolkit({
  show_derivation: {
    type: "backend",
    render: ({ args }) => {
      const steps = args.steps ?? [];
      return (
        <MathBlock
          label={args.label}
          steps={steps}
          visibleSteps={steps.length}
        />
      );
    },
  },
});

show_derivation has nothing to execute beyond returning its own arguments: the derivation is exactly what the model streamed, so the renderer reads args at every status and there is no separate result shape to design, and the matching server entry can be as small as execute: async (args) => args. See Defining tools for the full split between a tool's schema, its executor, and its renderer.

Let the message list render it

app/MyRuntimeProvider.tsx
import {
  AssistantRuntimeProvider,
  AuiConfig,
  Tools,
} from "@assistant-ui/react";
import { useChatRuntime } from "@assistant-ui/ai-sdk";
import { toolkit } from "./toolkit";

export function MyRuntimeProvider({
  children,
}: {
  children: React.ReactNode;
}) {
  const runtime = useChatRuntime();
  const config = AuiConfig({ tools: Tools({ toolkit }) });
  return (
    <AssistantRuntimeProvider runtime={runtime} config={config}>
      {children}
    </AssistantRuntimeProvider>
  );
}

Once the toolkit is registered, Thread and any custom message list built on assistant-ui's message part primitives (MessagePrimitive.Parts or MessagePrimitive.GroupedParts) render the registered UI automatically wherever the show_derivation call appears in the message, so nothing needs to be placed by hand.

Anatomy

<div data-slot="math-block">
  {/* optional label, monospace */}
  <span>{/* label */}</span>

  {/* one row per visible step, in array order */}
  <div>
    <span>{/* step.expression, a ReactNode */}</span>
    <span>{/* optional step.note */}</span>
  </div>
</div>

visibleSteps is floored and clamped into 0…steps.length: NaN or a negative number maps to 0, and any value past the array length maps to the array length, so an out of range or fractional count never throws and simply rounds down to a whole step. A step beyond the visible count is not in the DOM at all, not merely hidden, and each newly shown step fades and slides in over 300ms. label renders only when it is a truthy string.

Examples

Compose an expression with Frac, Sup, and Sub

A tool call can only stream plain text into expression, so a fraction or a superscript arriving at runtime renders as plain characters rather than a stacked layout. Building the richer visual with Frac, Sup, and Sub is a standalone concern: assemble the JSX yourself and pass it as a step's expression, and it renders the same way in either lane.

import { Frac, Sub, Sup } from "@/components/assistant-ui/elements/math-block";

const steps = [
  {
    expression: (
      <>
        p(x) ={" "}
        <Frac
          over={<>1</>}
          under={
            <>
              1 + e<Sup>−x</Sup>
            </>
          }
        />
      </>
    ),
    note: "the logistic",
  },
  {
    expression: (
      <>
        max<Sub>x</Sub> p′(x) = 0.25
      </>
    ),
  },
];

Where the steps come from

The model owns the derivation, so the backend entry only needs a schema wide enough to describe any worked answer; the executor hands the arguments straight back, and the renderer from the first step does the rest.

app/api/chat/route.ts
show_derivation: tool({
  description: "Show a worked, step by step derivation.",
  inputSchema: z.object({
    label: z.string().optional(),
    steps: z.array(
      z.object({ expression: z.string(), note: z.string().optional() }),
    ),
  }),
  execute: async (args) => args,
}),

Restyle the block

Both lanes take className on the root. The label uses the shared mono surface from surfaces.tsx, so restyling that token restyles every element that uses it.

<MathBlock className="max-w-none gap-4" /* ... */ />

API reference

Render props

PropTypeDescription
args{ label?: string; steps: { expression: string; note?: string }[] }The tool's arguments as streamed by the model, a partial parse while status.type is "running".
statusToolCallMessagePartStatus"running" while the model is still emitting args, "complete" once the call settles.

show_derivation arguments

FieldTypeDescription
labelstringOptional caption shown above the steps.
steps[].expressionstringRendered as plain text, assemble Frac, Sup, and Sub yourself if a step needs them.
steps[].notestringOptional small caption under the expression.