UI kit

RagSkeleton

DisplayReact · Next.js

Loading placeholders that match the shape of a chat reply, search results, or a streamed answer. Render it while the pipeline request is in flight to avoid layout shift.

<RagSkeleton />

Live preview

answer

chat

search

Installation

No dependencies to install — just React + Tailwind CSS. It's a presentational component (no data fetching), so the same source works unchanged in React and Next.js (App or Pages Router) — there's no separate variant. Copy it into your project (e.g. components/RagSkeleton.tsx) and import it.

Usage

usage.tsx
import { RagSkeleton } from "@/components/RagSkeleton";

export default function Example() {
  return (
    <RagSkeleton variant="chat" />
  );
}

Props

PropTypeDefaultDescription
variant"answer" | "chat" | "search""answer"Which layout to mimic.
linesnumber3Number of shimmer lines (answer/search).

Component source copy-paste ready

Presentational component — no data fetching, so the same source works unchanged in React and Next.js. There's no separate Next.js variant to copy.

RagSkeleton.tsx
"use client";

interface RagSkeletonProps {
  /** Which layout to mimic while loading. Default: "answer". */
  variant?: "answer" | "chat" | "search";
  /** Number of shimmer lines (answer / search variants). Default: 3. */
  lines?: number;
  className?: string;
}

function Bar({ w = "100%" }: { w?: string }) {
  return <div className="h-3 animate-pulse rounded bg-zinc-200 dark:bg-zinc-800" style={{ width: w }} />;
}

/**
 * RagSkeleton — loading placeholders that match the shape of a chat reply,
 * search results, or a streamed answer. Render it while the pipeline request
 * is in flight to avoid layout shift.
 */
export function RagSkeleton({ variant = "answer", lines = 3, className = "" }: RagSkeletonProps) {
  if (variant === "chat") {
    return (
      <div className={`flex flex-col gap-3 ${className}`}>
        <div className="flex justify-end">
          <div className="h-9 w-2/3 animate-pulse rounded-2xl bg-zinc-200 dark:bg-zinc-800" />
        </div>
        <div className="flex justify-start">
          <div className="h-16 w-5/6 animate-pulse rounded-2xl bg-zinc-100 dark:bg-zinc-800/60" />
        </div>
      </div>
    );
  }

  if (variant === "search") {
    return (
      <div className={`flex flex-col gap-2 ${className}`}>
        {Array.from({ length: lines }).map((_, i) => (
          <div key={i} className="rounded-lg border border-zinc-200 p-3 dark:border-zinc-800">
            <Bar w="40%" />
            <div className="mt-2">
              <Bar w="90%" />
            </div>
          </div>
        ))}
      </div>
    );
  }

  return (
    <div className={`flex flex-col gap-2 ${className}`}>
      {Array.from({ length: lines }).map((_, i) => (
        <Bar key={i} w={`${90 - i * 12}%`} />
      ))}
    </div>
  );
}