Home / Articles

Designing a Transparent RAG Interface with React, Tailwind and Motion

Build a RAG frontend that exposes retrieval stages, interactive citations, a source panel and post-answer actions using React state, Tailwind CSS and Framer Motion.

Publisher: REACTAPP.TOP11 min read2544 words

This article is published in English.

Sources: How I’d Build a Modern RAG Interface with React, Tailwind CSS & Framer Motion

A retrieval-augmented generation pipeline can be technically excellent and still feel like a weak product when the interface hides what the system is doing. A single chat box with a spinner tells the user nothing about which documents were searched, where the answer came from, or whether it is still being written. This walkthrough shows how to structure a React frontend around the real stages of a RAG request, so that retrieval, sources, streaming and citations become visible, inspectable parts of the experience.

Design around the stages of retrieval

The key shift is to treat the product as a sequence the user can follow rather than as "a chat": question, retrieval, sources, generation, citations, exploration. That sequence drives how the React application is organized.

For a typical internal knowledge assistant, the screen can be divided into three areas: a document library on the left, the conversation in the middle, and the sources for the current answer on the right.

---------------------------------------------------------
| Documents |          Conversation          | Sources |
|           |                                |         |
| SOPs      | User: How does onboarding work?| SOP.pdf |
| Policies  |                                |         |
| FAQs      | Searching knowledge...         | FAQ.pdf |
|           |                                |         |
|           | AI response...                 |         |
---------------------------------------------------------

It remains a chat interface, but the conversation is only one part of the product. Around it, the application also needs to expose:

  • the knowledge that is available to search
  • the current retrieval state
  • the sources used for an answer
  • document metadata
  • citations
  • follow-up actions
  • earlier conversations

Cramming all of that into one chat component quickly becomes unmanageable.

A component tree that matches the product

One workable structure looks like this:

RAGApplication
|
|-- Sidebar
|   |-- NewConversation
|   |-- ConversationHistory
|   `-- DocumentLibrary
|
|-- ChatWorkspace
|   |-- MessageList
|   |-- RetrievalStatus
|   |-- StreamingResponse
|   |-- CitationList
|   `-- PromptInput
|
`-- SourcePanel
    |-- SourceCard
    |-- DocumentPreview
    |-- RetrievedChunk
    `-- SourceMetadata

The split reflects how differently each region behaves. Messages accumulate like a conversation, the retrieval indicator is short-lived, the answer streams in, the source panel depends on the selected citation, and the library works like a dashboard. Separate components let you manage each one's state independently and keep the streaming answer from re-rendering the whole screen on every token.

Step 1: Return enough data from the API

Animations and layout come later. First, the backend has to give the frontend something to work with. A response that contains only the generated text leaves the UI with nothing to show except that text:

{
  "answer": "..."
}

A richer contract includes a status and the list of sources that grounded the answer, each with an identifier, a title, a section reference and a short snippet:

{
  "answer": "Enterprise customers can request...",
  "status": "complete",
  "sources": [
    {
      "id": "doc-1",
      "title": "Refund Policy",
      "section": "3.2",
      "snippet": "Refund requests must be submitted..."
    },
    {
      "id": "doc-2",
      "title": "Enterprise Terms",
      "section": "5",
      "snippet": "Enterprise subscriptions..."
    }
  ]
}

With this shape, React can render the answer, show citation chips, open individual sources and preview the relevant passage, making the evidence a first-class interaction rather than an afterthought. Insist on the stable id: it becomes the React key for source cards and the handle for opening a document.

Step 2: Model retrieval as explicit UI state

A RAG request passes through several distinct phases, and it helps to name them explicitly:

idle
↓
retrieving
↓
sources_found
↓
generating
↓
complete

The simplest starting point in React is a few pieces of state for the current phase, the sources and the answer:

const [status, setStatus] = useState("idle");
const [sources, setSources] = useState([]);
const [answer, setAnswer] = useState("");

When the user submits a question, the handler moves through those phases as the request progresses:

async function handleAsk(question) {
  setStatus("retrieving");
  const result = await askKnowledgeBase(question);  setSources(result.sources);
  setStatus("generating");  setAnswer(result.answer);
  setStatus("complete");
}

This is deliberately simplified. Because one call returns both sources and answer, generating is replaced by complete in the same tick, so React batches the updates and the user never sees the middle state (and sources_found is never set at all). A streaming implementation would instead emit a retrieval-finished event, then answer tokens, then completion. The handler also needs a try/catch and an error status so a failed request does not spin forever. Even so, an explicit status lets the UI say exactly what is happening, and as phases multiply, a useReducer with defined transitions stays consistent more easily than several useState calls.

Step 3: Replace the spinner with a retrieval indicator

A bare "Loading..." message says almost nothing. A RAG application can narrate its pipeline instead. First the user sees that the knowledge base is being searched:

Searching your knowledge base...

then how much evidence was found:

Found 4 relevant sources

and finally that the answer is being written:

Generating response...

That small change explains the pipeline and sets expectations about the wait. A minimal component maps the status to a message:

function RetrievalStatus({ status, sourceCount }) {
  if (status === "retrieving") {
    return <p>Searching your knowledge base...</p>;
  }
  if (status === "generating") {
    return <p>Found {sourceCount} sources. Generating response...</p>;
  }  return null;
}

Nothing clever, and that is the point: the component returns null once there is nothing to report, yet the app immediately feels more deliberate. Placing the message in an aria-live="polite" region lets screen-reader users hear the same updates.

Step 4: Build visual hierarchy with Tailwind

RAG screens get crowded quickly with user and model messages, document names and snippets, citations, warnings, actions and retrieval status. Tailwind's utility classes make it fast to establish a clear hierarchy without a large custom stylesheet. A source card, for example, can use a bordered, lightly shadowed container with a bold title:

<div className="rounded-xl border border-slate-200 bg-white p-4 shadow-sm">
  <p className="text-sm font-semibold text-slate-900">
    Refund Policy
  </p>

followed by a muted section label and a readable snippet with generous line height:

  <p className="mt-1 text-xs text-slate-500">
    Section 3.2
  </p>  <p className="mt-3 text-sm leading-6 text-slate-700">
    Refund requests must be submitted within...
  </p>
</div>

The styling is intentionally plain. Clarity beats decoration in AI interfaces, because the user wants to know three things: the answer, its origin, and the possible next step. Visual weight should follow that order.

Step 5: Make citations interactive

Many RAG interfaces stop at a bracketed number such as [1]. A citation is far more useful when it lets the user inspect the evidence behind a claim. Rendering each citation as a button that records the selected source is a simple way to do that:

<button
  onClick={() => setSelectedSource(source)}
  className="rounded-full border px-3 py-1 text-sm"
>
  Refund Policy · 3.2
</button>

Clicking the chip opens the source panel, which shows the document title:

Refund Policy

along with the section, the quoted passage and a way to open the full document:

Section 3.2"Refund requests must be submitted within..."[Open document]

Citations now do real work, and the grounding of the answer becomes visible: users can check a claim in one click instead of taking it on trust. A real button element also keeps citations reachable by keyboard.

Step 6: Use motion to explain what changed

Framer Motion earns its place in this kind of interface only when animation communicates state. Nothing should bounce for its own sake. A good use is letting source cards fade and slide up slightly when retrieval completes. Start by importing the motion component:

import { motion } from "framer-motion";

Then wrap each card in a motion.div that animates from transparent and offset to fully visible over a quarter of a second:

<motion.div
  initial={{ opacity: 0, y: 8 }}
  animate={{ opacity: 1, y: 0 }}
  transition={{ duration: 0.25 }}
>
  <SourceCard source={source} />
</motion.div>

The source panel can slide in from the side when a citation is selected, and slide back out when it closes:

<motion.aside
  initial={{ x: 40, opacity: 0 }}
  animate={{ x: 0, opacity: 1 }}
  exit={{ x: 40, opacity: 0 }}
>
  <SourcePanel source={selectedSource} />
</motion.aside>

One caveat: the exit animation only runs when the component is rendered inside AnimatePresence, which keeps the element mounted until its exit animation finishes. Without it, React removes the panel immediately and the slide-out never plays. Also note that, at the time of writing, Framer Motion is distributed as the motion package with a motion/react entry point; check the current docs for the import path that matches your installed version.

The value lies in the message the movement carries: something changed, new information arrived, and this source belongs to this answer. Honour the reduced-motion preference so it never becomes a nuisance.

Step 7: Treat streaming differently from loading

Streaming creates its own UX problem. When text arrives token by token, the interface must make it obvious that the answer is still being written. A small pulsing caret after the text does this well. The component renders the content so far:

function StreamingAnswer({ content, isStreaming }) {
  return (
    <div className="prose max-w-none">
      {content}

and appends the caret only while streaming is active:

      {isStreaming && (
        <span className="ml-1 inline-block h-4 w-1 animate-pulse bg-slate-500" />
      )}
    </div>
  );
}

It is also sensible to limit what the user can do with a half-finished answer. Reading actions can stay available, while anything that commits the answer somewhere else waits for completion:

Copy answer       ✓
View sources      ✓
Create task       disabled while streaming
Export            disabled while streaming

The goal is honest communication of state, not polish: exporting a half-written answer would produce a broken result.

Step 8: Make the source panel part of the product

A proper source panel is one of the strongest features a RAG interface can offer. Consider the full journey:

User asks question
        ↓
RAG retrieves 4 chunks
        ↓
AI generates answer
        ↓
Citation cards appear
        ↓
User clicks citation
        ↓
Relevant document section opens

At minimum, the panel can show:

  • the document title
  • the section
  • the retrieved passage
  • the document type
  • relevance information
  • a link or button to open the document

Over time it can grow to include:

  • highlighting of the matching text
  • neighbouring chunks for context
  • a document preview
  • metadata filters

Each addition makes the system less of a black box. A good panel also exposes weak retrieval, which is one reason to invest in source quality early in a RAG project.

Step 9: Offer actions after the answer

This is where a RAG chatbot starts turning into an application. Suppose someone asks how the team onboards a new client. The system retrieves the onboarding SOP and responds with a checklist:

1. Create workspace
2. Request account access
3. Confirm scope
4. Assign team
5. Schedule kickoff

Instead of ending there, the interface can offer a link back to the source document:

[View SOP]

plus actions that reuse the result:

[Copy checklist][Create task list]

The task-list action is the interesting one. The value of an AI system often lies less in the answer than in the next thing it enables, and this is where RAG applications start drifting toward agents. Our guide to building multi-step AI agent UIs covers that direction.

The complete interaction, end to end

Putting everything together, a single question travels through the whole stack. It starts with the user asking whether an enterprise client can cancel after 30 days:

USER
"Can an enterprise client cancel after 30 days?"

From there, React creates the query state, the RAG API retrieves documents, the status line reports the search, the source count appears, the LLM generates a grounded answer that streams onto the screen, citation chips appear, and a click on one slides the source panel into view with the relevant passage:

        ↓REACT
Creates query state        ↓RAG API
Retrieves relevant documents        ↓UI STATUS
"Searching your knowledge base..."        ↓SOURCES
3 relevant sources found        ↓LLM
Generates grounded response        ↓STREAMING UI
Response appears progressively        ↓CITATIONS
Refund Policy · 3.2
Enterprise Terms · 5        ↓USER
Clicks citation        ↓MOTION
Source panel slides into view        ↓SOURCE
Relevant passage displayed

Think of it as two cooperating layers: an intelligence layer on the backend that retrieves and generates, and an experience layer on the frontend that makes the work understandable. They are built separately but must be designed together, which is why the Step 1 contract matters.

A pragmatic technology stack

A lightweight version of this application can be built with:

  • Frontend: React, Tailwind CSS and Framer Motion.
  • Backend: FastAPI on Python.
  • Retrieval: embeddings and a vector database, orchestrated with LangChain or a small custom pipeline.
  • Model: any suitable hosted API, such as OpenAI, Gemini or Claude.
  • Storage: PostgreSQL or Supabase, with Qdrant or a similar vector store.

There is no need to adopt all of it on day one. For a portfolio project, a minimal starting point is enough:

React
+
Tailwind
+
Framer Motion
+
FastAPI
+
one LLM
+
one vector store

Retrieval quality, reranking, authentication, document processing and actions can be improved incrementally once the core loop works.

The pattern to avoid

The version worth steering away from looks like this:

Upload PDF
↓
Chat box
↓
Spinner
↓
Answer

It proves the pipeline works but shows little product thinking: nothing is visible, verifiable or actionable. A stronger target combines several layers:

Knowledge
+
Retrieval
+
Transparent states
+
Citations
+
Source exploration
+
Useful actions

That combination demonstrates both AI engineering and solid frontend implementation.

Why the interface layer deserves the attention

The model is only half of an AI product. The rest is what good software has always required: understandable interactions, clear feedback, predictable states, useful workflows and sound information architecture. React, Tailwind and Motion contribute none of the intelligence, yet they decide whether anyone can benefit from it, and that frontend work is routinely undervalued.

Key takeaways

  • Design the API response first; sources with stable IDs, sections and snippets are what make citations and source panels possible.
  • Model the request as named phases and plan for streaming and errors, rather than toggling a single loading flag.
  • Treat citations as buttons that open evidence, and give the source panel real product attention.
  • Use animation only to signal change, wrap exiting elements in AnimatePresence, and honour reduced-motion preferences.
  • Gate actions that depend on a finished answer, and offer next steps once it is complete, because that is where a chatbot becomes an application.