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 threadCode
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
| Element | Light | Dark |
|---|---|---|
| Background | #fdfdfd | #141414 |
| Input bg | #f8f8f8 | #212121 |
| Text | #0d0d0d | white |
| Muted | #9a9a9a | #6b6b6b |
| Ring border | #e5e5e5 | #2a2a2a |
| Send button | #0d0d0d | white |
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.