---
title: Gemini Clone
description: Open-source Gemini clone in React with a centered greeting over an ambient glow, a single-row pill composer, avatar-free assistant replies, and disabled, ready, and stop send states.
---

import { Gemini } from "@/components/examples/gemini";

<div className="not-prose h-[600px]">
  <Gemini />
</div>

## Overview

The Gemini Clone shows how to restyle assistant-ui to match Google Gemini's current interface. The empty state centers a single-line greeting over a soft blue glow, and a single-row pill composer is shared by both the empty and chat states. Assistant replies render as full-width markdown with no avatar, and user turns sit in a rounded grey bubble.

## Features

- **Centered greeting**: one "How can I help you today?" headline, centered over a blurred radial glow.
- **Single-row composer**: a 32px pill that holds the `+` menu, the input, the model picker, the mic, and send on one line.
- **Combined `+` menu**: "Add photos & files" sits next to the Deep Research, Canvas, Create image, and Guided Learning tools.
- **Model picker**: Fast and Thinking, each with a short description and a check mark on the active model.
- **Send states**: a disabled grey arrow when empty, a blue arrow when ready, and a stop square while a response streams.
- **Avatar-free replies**: assistant messages are full-width markdown, with the action bar revealed on hover.
- **Grey user bubble**: a `rounded-3xl` warm-grey bubble, right-aligned, with copy and edit actions on hover.

## Quick Start

```bash
npx assistant-ui add thread
```

## Code

The empty and chat states render the same `Composer`. It is a flex column: an optional attachment row on top, then a single action row with the input as the `flex-1` middle and the controls bottom-aligned around it.

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

export const Gemini = () => (
  <ThreadPrimitive.Root className="bg-[#fdfcfc] dark:bg-[#131314]">
    <AuiIf condition={(s) => s.thread.isEmpty}>
      <EmptyState />
    </AuiIf>
    <AuiIf condition={(s) => !s.thread.isEmpty}>
      <ThreadPrimitive.Viewport>
        <ThreadPrimitive.Messages components={{ Message: ChatMessage }} />
        <ThreadPrimitive.ViewportFooter className="sticky bottom-0">
          <Composer />
        </ThreadPrimitive.ViewportFooter>
      </ThreadPrimitive.Viewport>
    </AuiIf>
  </ThreadPrimitive.Root>
);

const Composer = () => (
  <ComposerPrimitive.Root className="flex flex-col rounded-4xl bg-white p-3 shadow-[0_2px_10px_-2px_rgba(0,0,0,0.18)] dark:bg-[#1e1f20]">
    <AuiIf condition={(s) => s.composer.attachments.length > 0}>
      <ComposerPrimitive.Attachments
        components={{ Attachment: GeminiAttachment }}
      />
    </AuiIf>
    <div className="flex items-end gap-1">
      <PlusMenu />
      <ComposerPrimitive.Input
        placeholder="Ask Gemini"
        className="flex-1 resize-none bg-transparent text-[17px]"
      />
      <ModelPicker />
      <VoiceButton />
      <SendButton />
    </div>
  </ComposerPrimitive.Root>
);
```

### Send states

The trailing slot swaps between Send and Cancel with `AuiIf`. `ComposerPrimitive.Send` disables itself while the composer is empty, so the disabled, ready, and running looks are all driven by state instead of manual class toggling:

```tsx
{/* Send: disabled grey when empty, blue when there is text */}
<AuiIf condition={(s) => !s.thread.isRunning}>
  <ComposerPrimitive.Send className="bg-[#d3e3fd] text-[#062e6f] disabled:bg-[#e8eaed] disabled:text-[#1f1f1f]/40">
    <ArrowUpIcon />
  </ComposerPrimitive.Send>
</AuiIf>
{/* Cancel: stop square while a response streams */}
<AuiIf condition={(s) => s.thread.isRunning}>
  <ComposerPrimitive.Cancel className="bg-[#d3e3fd] text-[#062e6f]">
    <span className="size-3 rounded-[3px] bg-current" />
  </ComposerPrimitive.Cancel>
</AuiIf>
```

### Combined `+` menu

The `+` button opens one menu for both attachments and tools. `ComposerPrimitive.AddAttachment` is rendered as the first item through the `render` prop, so picking it opens the file dialog and closes the menu:

```tsx
import {
  DropdownMenu,
  DropdownMenuContent,
  DropdownMenuItem,
  DropdownMenuSeparator,
  DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";

<DropdownMenu>
  <DropdownMenuTrigger>
    <PlusIcon />
  </DropdownMenuTrigger>
  <DropdownMenuContent align="start" side="bottom">
    <DropdownMenuItem render={<ComposerPrimitive.AddAttachment />}>
      <Paperclip /> Add photos & files
    </DropdownMenuItem>
    <DropdownMenuSeparator />
    <DropdownMenuItem>
      <Telescope /> Deep Research
    </DropdownMenuItem>
    <DropdownMenuItem>
      <PencilRuler /> Canvas
    </DropdownMenuItem>
    {/* Create image, Guided Learning ... */}
  </DropdownMenuContent>
</DropdownMenu>
```

### Ambient glow

The empty state places a single blurred ellipse behind the greeting and composer. It is decorative, so it is `pointer-events-none` and `aria-hidden`:

```tsx
<div className="relative flex grow items-center justify-center">
  <div
    aria-hidden="true"
    className="pointer-events-none absolute top-1/2 left-1/2 h-[330px] w-[720px] -translate-x-1/2 -translate-y-1/2 bg-[radial-gradient(closest-side,#a9d1fb,transparent)] opacity-70 blur-[55px] dark:bg-[radial-gradient(closest-side,#1d4068,transparent)] dark:opacity-65"
  />
  <div className="relative z-10">
    <h1 className="text-center text-4xl">How can I help you today?</h1>
    <Composer />
  </div>
</div>
```

### Color palette

| Element            | Light                 | Dark      |
| ------------------ | --------------------- | --------- |
| Background         | `#fdfcfc`             | `#131314` |
| Composer surface   | `#ffffff`             | `#1e1f20` |
| Primary text       | `#1f1f1f`             | `#e3e3e3` |
| Muted text & icons | `#444746`             | `#c4c7c5` |
| User bubble        | `#f2f0f0`             | `#333537` |
| Send (ready)       | `#d3e3fd`             | `#1f3760` |
| Ambient glow       | `#a9d1fb`             | `#1d4068` |

### Messages

Assistant replies have no avatar; the markdown spans the full content width and the action bar (feedback, copy, regenerate, more) fades in on hover. User turns sit in a rounded grey bubble, right-aligned, with copy and edit actions to its left:

```tsx
<div className="max-w-[75%] rounded-3xl bg-[#f2f0f0] px-5 py-3 dark:bg-[#333537]">
  <MessagePrimitive.Parts components={{ Text: MarkdownText }} />
</div>
```

## Source

<SourceLink href="https://github.com/assistant-ui/assistant-ui/blob/main/apps/docs/components/examples/gemini.tsx" />
