Examples

Grok Clone

Open-source Grok clone in React. Pill composer with paperclip, animated Mic and Send swap, functional model picker dropdown, and message timing tooltip.

open demosource

Grok can make mistakes. Verify important information.

fig. 01live

Overview

The Grok Clone demonstrates how to customize assistant-ui to match xAI's Grok interface. The empty state centers the Grok wordmark above a pill composer with an animated trailing button that swaps between Mic, Send, and Cancel based on composer state. Models are picked from a real dropdown that animates open/collapse along with the composer.

Features

  • Centered Grok Wordmark: Empty state shows the SVG wordmark above the composer
  • Pill Composer: rounded-4xl with thin ring border, paperclip on the leading edge
  • Functional Model Picker: Fast / Grok 4.7 / Think dropdown with descriptions, plus "Subscribe to SuperGrok"
  • Expand/Collapse Model Pill: When the composer is empty the pill shows the model name; when typing it collapses to the model icon
  • Animated Send Slot: Mic when empty, Send (white-on-dark) when typing, Cancel while running, all driven by group-data-[empty] / group-data-[running]
  • Inverted Primary: Dark button on light mode, light button on dark mode
  • Message Timing: Streaming responses show a hover tooltip with first-token, total time, tokens/sec, chunk count
  • Hover-only Action Bars: Edit/Copy on user messages, Reload/Copy/Good response/Bad response on assistant messages

Quick Start

npx assistant-ui add thread

Code

The composer carries data-empty and data-running attributes so the trailing button transitions smoothly:

import {
  AuiIf,
  ThreadPrimitive,
  ComposerPrimitive,
  useAuiState,
} from "@assistant-ui/react";

export const Grok = () => (
  <ThreadPrimitive.Root className="bg-[#fdfdfd] px-4 dark:bg-[#141414]">
    <AuiIf condition={(s) => s.thread.isEmpty}>
      <div className="flex h-full flex-col items-center justify-center">
        <GrokLogo className="mb-6 h-10 text-[#0d0d0d] dark:text-white" />
        <Composer />
      </div>
    </AuiIf>
    <AuiIf condition={(s) => !s.thread.isEmpty}>
      <ThreadPrimitive.Viewport>
        <ThreadPrimitive.Messages>
          {() => <ChatMessage />}
        </ThreadPrimitive.Messages>
      </ThreadPrimitive.Viewport>
      <Composer />
    </AuiIf>
  </ThreadPrimitive.Root>
);

const Composer = () => {
  const isEmpty = useAuiState((s) => s.composer.isEmpty);
  const isRunning = useAuiState((s) => s.thread.isRunning);
  return (
    <ComposerPrimitive.Root
      className="group/composer mx-auto mb-3 w-full max-w-3xl"
      data-empty={isEmpty}
      data-running={isRunning}
    >
      <div className="rounded-4xl bg-[#f8f8f8] ring-1 ring-[#e5e5e5] ring-inset dark:bg-[#212121] dark:ring-[#2a2a2a]">
        <ComposerPrimitive.AddAttachment><Paperclip /></ComposerPrimitive.AddAttachment>
        <ComposerPrimitive.Input placeholder="What do you want to know?" />
        <GrokModelPicker />
        <SendOrMic />
      </div>
    </ComposerPrimitive.Root>
  );
};

Functional Model Picker

const MODELS = [
  { id: "fast", name: "Fast", description: "Default. Quick responses", Icon: Zap },
  { id: "grok-4.7", name: "Grok 4.7", description: "Standard reasoning", Icon: Moon },
  { id: "think", name: "Think", description: "Multi-step reasoning", Icon: Moon },
];

<DropdownMenu>
  <DropdownMenuTrigger>
    <CurrentIcon />
    {/* Name + chevron collapse to nothing while typing */}
    <div className="group-data-[empty=false]/composer:max-w-0 group-data-[empty=true]/composer:max-w-32">
      <span>{current.name}</span>
      <ChevronDown />
    </div>
  </DropdownMenuTrigger>
  <DropdownMenuContent>
    {MODELS.map((m) => (
      <DropdownMenuItem onClick={() => setModel(m.id)}>
        {m.id === model ? <Check /> : <m.Icon />}
        {m.name} — {m.description}
      </DropdownMenuItem>
    ))}
  </DropdownMenuContent>
</DropdownMenu>

Animated Send Slot

<div className="relative h-9 w-9 rounded-full bg-[#0d0d0d] dark:bg-white">
  <button className="group-data-[empty=false]/composer:scale-0">
    <Mic /> {/* Voice when empty */}
  </button>
  <ComposerPrimitive.Send className="group-data-[empty=true]/composer:scale-0">
    <ArrowUp /> {/* Send when typing */}
  </ComposerPrimitive.Send>
  <ComposerPrimitive.Cancel className="group-data-[running=false]/composer:scale-0">
    <Square /> {/* Stop while running */}
  </ComposerPrimitive.Cancel>
</div>

Color Palette

ElementLightDark
Background#fdfdfd#141414
Composer surface#f8f8f8#212121
Ring border#e5e5e5#2a2a2a
Primary text#0d0d0dwhite
Muted text#9a9a9a#6b6b6b
Send button (inverted)#0d0d0dwhite

Animation Technique

The composer uses data-* attributes for state-based animations:

<ComposerPrimitive.Root data-empty={isEmpty} data-running={isRunning}>
  <button className="group-data-[empty=false]/composer:scale-0 group-data-[empty=false]/composer:opacity-0" />
</ComposerPrimitive.Root>

The same attributes drive the Mic and Send transition and the model pill expand/collapse.

Source

View full source on GitHub