Examples

Gemini Clone

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

open demosource

How can I help you today?

fig. 01live

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 above the composer and its blurred 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: Flash and Thinking, each with a short description and a check mark on the active model.
  • Send states: no button when empty, a blue arrow when there is text, 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

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.

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

export const Gemini = () => (
  <ThreadPrimitive.Root className="bg-[#fdfcfc] dark:bg-[#0c0c0c]">
    <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 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. Send renders only when the composer has text and no response is streaming, so the slot is empty until the user types:

{/* Send: blue arrow when there is text */}
<AuiIf condition={(s) => !s.thread.isRunning && !s.composer.isEmpty}>
  <ComposerPrimitive.Send className="bg-[#1f3b9b] text-white 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-[#1f3b9b] text-white">
    <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:

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 shape behind the composer, below the greeting. It is decorative, so it is pointer-events-none and aria-hidden:

<p className="relative z-10 mb-6 text-center text-4xl">How can I help you today?</p>
<div className="relative">
  <div
    aria-hidden="true"
    className="pointer-events-none absolute top-1/2 left-1/2 h-[260px] w-[680px] max-w-[92%] -translate-x-1/2 -translate-y-1/2 rounded-[140px] bg-[#a9d1fb]/60 blur-[90px] dark:bg-[#1b2f9c]/50"
  />
  <div className="relative z-10">
    <Composer />
  </div>
</div>

Color palette

ElementLightDark
Background#fdfcfc#0c0c0c
Composer surface#ffffff#1e1f20
Primary text#1f1f1f#e3e3e3
Muted text & icons#444746#c4c7c5
User bubble#f2f0f0#333537
Send#1f3b9b#1f3b9b
Ambient glow#a9d1fb#1b2f9c

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:

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

Source

View full source on GitHub