Grok Clone

Customized colors and styles for a Grok look and feel

Overview

The Grok Clone demonstrates how to customize assistant-ui to match xAI's Grok interface. This example features a clean, minimalist design with smooth animations, inverted color schemes for light/dark modes, and a distinctive welcome screen with the Grok logo.

Features

  • Minimalist Design: Clean interface with subtle ring borders
  • Animated Composer: Smooth transitions between send, voice, and cancel states
  • Empty State: Welcome screen with centered Grok logo
  • Attachment Thumbnails: Image preview with hover-to-remove
  • Inverted Buttons: Dark button on light mode, light button on dark mode
  • Model Selector: Animated expand/collapse on focus

Quick Start

npx assistant-ui add thread

Code

The Grok clone uses clean minimalism with smooth animations:

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

export const Grok = () => {
  return (
    <ThreadPrimitive.Root className="flex h-full flex-col bg-[#fdfdfd] px-4 dark:bg-[#141414]">
      <ThreadPrimitive.Empty>
        <div className="flex h-full flex-col items-center justify-center">
          <GrokLogo className="mb-6 h-10 text-[#0d0d0d] dark:text-white" />
          <Composer />
        </div>
      </ThreadPrimitive.Empty>

      <ThreadPrimitive.Viewport className="flex grow flex-col overflow-y-scroll pt-16">
        <ThreadPrimitive.Messages components={{ Message: ChatMessage }} />
      </ThreadPrimitive.Viewport>
      <Composer />
    </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.Input
          placeholder="What do you want to know?"
          className="w-full bg-transparent text-[#0d0d0d] outline-none dark:text-white"
        />
        {/* Animated button with three states */}
        <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 mode when empty */}
          </button>
          <ComposerPrimitive.Send className="group-data-[empty=true]/composer:scale-0">
            <ArrowUpIcon /> {/* Send when has text */}
          </ComposerPrimitive.Send>
          <ComposerPrimitive.Cancel className="group-data-[running=false]/composer:scale-0">
            <Square /> {/* Stop when running */}
          </ComposerPrimitive.Cancel>
        </div>
      </div>
    </ComposerPrimitive.Root>
  );
};

Color Palette

ElementLightDark
Background#fdfdfd#141414
Input bg#f8f8f8#212121
Text#0d0d0dwhite
Muted#9a9a9a#6b6b6b
Ring border#e5e5e5#2a2a2a
Send button#0d0d0dwhite

Animation Technique

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

// Container passes state as data attributes
<ComposerPrimitive.Root data-empty={isEmpty} data-running={isRunning}>

// Children animate based on parent's data attributes
<button className="group-data-[empty=false]/composer:scale-0 group-data-[empty=false]/composer:opacity-0">

This creates smooth transitions between voice → send → cancel button states.

Source

View full source on GitHub