grok
Type
External
Status
Published
Created
Mar 17, 2026
Updated
May 9, 2026
Updated by
Dosu Bot
Source
View

import { Grok } from "@/components/examples/grok";

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.1 / 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 just the model icon
  • Animated Send Slot: Mic when empty, Send (white-on-dark) when typing, Cancel while running — all via 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/👍/👎 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.1", name: "Grok 4.1", 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 onSelect={() => 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>

This drives the Mic↔Send transition and the model pill expand/collapse on the same gesture.

Source#