Scrim UI

Source Card UI

A cited source with favicon, title, domain and snippet.

sourcecitationreference

Props

1

Component source

Single-file React + Tailwind component. No dependencies — drop it into any project with Tailwind configured.

source-card.tsx
"use client";

import * as React from "react";

/* ------------------------------------------------------------------ */
/* Types                                                               */
/* ------------------------------------------------------------------ */

export type SourceCardProps = {
  title: string;
  url: string;
  domain?: string;
  snippet?: string;
  favicon?: string;
  index?: number;
  className?: string;
};

/* ------------------------------------------------------------------ */
/* Helpers                                                             */
/* ------------------------------------------------------------------ */

const FALLBACK_COLORS = [
  "bg-slate-500",
  "bg-emerald-500",
  "bg-amber-500",
  "bg-rose-500",
  "bg-indigo-500",
  "bg-teal-500",
];

function colorFor(title: string) {
  let h = 0;
  for (let i = 0; i < title.length; i++) h = (h * 31 + title.charCodeAt(i)) >>> 0;
  return FALLBACK_COLORS[h % FALLBACK_COLORS.length];
}

function domainFromUrl(url: string) {
  try {
    return new URL(url).hostname.replace(/^www\./, "");
  } catch {
    return url;
  }
}

/* ------------------------------------------------------------------ */
/* SourceCard                                                          */
/* ------------------------------------------------------------------ */

export function SourceCard({
  title,
  url,
  domain,
  snippet,
  favicon,
  index,
  className = "",
}: SourceCardProps) {
  const host = domain ?? domainFromUrl(url);

  return (
    <a
      href={url}
      target="_blank"
      rel="noreferrer noopener"
      className={`group block rounded-xl border border-zinc-200 bg-white p-3.5 transition-colors hover:border-zinc-300 hover:bg-zinc-50 dark:border-zinc-800 dark:bg-zinc-900 dark:hover:border-zinc-700 dark:hover:bg-zinc-800/60 ${className}`}
    >
      <div className="flex items-start gap-2.5">
        {favicon ? (
          // eslint-disable-next-line @next/next/no-img-element
          <img
            src={favicon}
            alt=""
            className="h-5 w-5 shrink-0 rounded"
            width={20}
            height={20}
          />
        ) : (
          <span
            className={`flex h-5 w-5 shrink-0 select-none items-center justify-center rounded text-[9px] font-bold text-white ${colorFor(title)}`}
          >
            {title.slice(0, 1).toUpperCase()}
          </span>
        )}
        <div className="min-w-0">
          <p className="truncate text-sm font-medium text-zinc-800 group-hover:underline dark:text-zinc-100">
            {title}
          </p>
          <p className="mt-0.5 text-xs text-zinc-500 dark:text-zinc-400">{host}</p>
          {snippet && (
            <p className="mt-1.5 line-clamp-2 text-[13px] leading-5 text-zinc-500 dark:text-zinc-400">
              {snippet}
            </p>
          )}
        </div>
        {index !== undefined && (
          <span className="ml-auto flex h-5 w-5 shrink-0 items-center justify-center rounded bg-zinc-100 text-[11px] font-medium text-zinc-600 dark:bg-zinc-800 dark:text-zinc-400">
            {index}
          </span>
        )}
      </div>
    </a>
  );
}

Usage Guidelines

  • Always show the domain — it's the fastest relevance signal, before the title or snippet.
  • Number cards so the answer text can cite them with plain [1][2] markers.
  • Open in a new tab with rel="noreferrer noopener" — sources are third-party destinations.
  • Keep snippets to two lines; truncate rather than expanding the card.
  • Fall back to a colored letter avatar when a favicon isn't available — never a broken image icon.

Common UX Mistakes

  • Hiding the source until hover — citations only work when the source is glanceable.
  • Showing favicon-less broken images; always have a graceful fallback.
  • Making the whole card a link that swallows the citation number as the anchor.
  • Dropping the snippet in a full-screen modal instead of an inline card.

Related Components