errors
Console Error Card
A structured card surfacing a browser console error with a code frame, call stack, and navigation.
1/2Console Error
Encountered a script tag while rendering React component. Scripts inside React components are never executed when rendering on the client. Consider using template tag instead.
src/app/layout.tsx (43:9) @ RootLayout
41 >
42 <head>
>43 <script dangerouslySetInnerHTML={{ __html: THEME_INIT_SCRIPT }} />
44 </head>
45 <body className="flex min-h-full flex-col font-sans">
46 <header className="border-b">
Was this helpful?
"use client";
import * as React from "react";
import { AnimatePresence, motion } from "motion/react";
import { ChevronLeft, ChevronRight, ChevronDown, Copy, ThumbsUp, ThumbsDown, X, ExternalLink } from "lucide-react";
import { cn } from "@/lib/utils";
export interface CodeLine {
number: number;
content: string;
isError?: boolean;
}
export interface CodeFrame {
file: string;
lines: CodeLine[];
}
export interface StackFrame {
name: string;
context?: string;
file?: string;
}
export interface ConsoleErrorCardProps {
type?: "error" | "warning";
message: string;
frame?: CodeFrame;
stack?: StackFrame[];
current?: number;
total?: number;
onPrev?: () => void;
onNext?: () => void;
onClose?: () => void;
onCopy?: () => void;
onOpenFrame?: (file: string) => void;
onHelpful?: (value: boolean) => void;
className?: string;
}
export function ConsoleErrorCard({
type = "error",
message,
frame,
stack = [],
current = 1,
total = 1,
onPrev,
onNext,
onClose,
onCopy,
onOpenFrame,
onHelpful,
className,
}: ConsoleErrorCardProps) {
const [stackOpen, setStackOpen] = React.useState(false);
const [helpfulVote, setHelpfulVote] = React.useState<boolean | null>(null);
const [copied, setCopied] = React.useState(false);
function handleCopy() {
onCopy?.();
setCopied(true);
setTimeout(() => setCopied(false), 1500);
}
function handleHelpful(value: boolean) {
setHelpfulVote(value);
onHelpful?.(value);
}
const isError = type === "error";
return (
<div
className={cn(
"w-full overflow-hidden rounded-2xl border border-border bg-background text-foreground",
className
)}
>
{/* Top bar */}
<div className="flex items-center justify-between border-b border-border px-3.5 py-2.5">
<div className="flex items-center gap-2">
{total > 1 && (
<>
<button
type="button"
onClick={onPrev}
disabled={current <= 1}
aria-label="Previous error"
className="rounded-md p-0.5 text-muted-foreground transition-colors hover:bg-accent hover:text-foreground disabled:pointer-events-none disabled:opacity-40"
>
<ChevronLeft className="size-4" aria-hidden />
</button>
<span className="font-mono text-[11px] text-muted-foreground tabular-nums">
{current}/{total}
</span>
<button
type="button"
onClick={onNext}
disabled={current >= total}
aria-label="Next error"
className="rounded-md p-0.5 text-muted-foreground transition-colors hover:bg-accent hover:text-foreground disabled:pointer-events-none disabled:opacity-40"
>
<ChevronRight className="size-4" aria-hidden />
</button>
</>
)}
<span
className={cn(
"rounded-md px-2 py-0.5 text-[11px] font-semibold",
isError
? "bg-red-100 text-red-700 dark:bg-red-950/60 dark:text-red-400"
: "bg-amber-100 text-amber-700 dark:bg-amber-950/60 dark:text-amber-400"
)}
>
Console {isError ? "Error" : "Warning"}
</span>
</div>
<div className="flex items-center gap-1">
{onCopy && (
<button
type="button"
onClick={handleCopy}
aria-label="Copy error"
className="rounded-md p-1.5 text-muted-foreground transition-colors hover:bg-accent hover:text-foreground"
>
<AnimatePresence mode="wait" initial={false}>
{copied ? (
<motion.svg
key="check"
initial={{ scale: 0.7, opacity: 0 }}
animate={{ scale: 1, opacity: 1 }}
exit={{ scale: 0.7, opacity: 0 }}
transition={{ duration: 0.15 }}
className="size-4 text-emerald-500"
viewBox="0 0 16 16"
fill="none"
>
<path d="M3 8l3.5 3.5L13 5" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
</motion.svg>
) : (
<motion.div key="copy" initial={{ scale: 0.7, opacity: 0 }} animate={{ scale: 1, opacity: 1 }} exit={{ scale: 0.7, opacity: 0 }} transition={{ duration: 0.15 }}>
<Copy className="size-4" aria-hidden />
</motion.div>
)}
</AnimatePresence>
</button>
)}
{onClose && (
<button
type="button"
onClick={onClose}
aria-label="Close"
className="rounded-md p-1.5 text-muted-foreground transition-colors hover:bg-accent hover:text-foreground"
>
<X className="size-4" aria-hidden />
</button>
)}
</div>
</div>
{/* Error message */}
<div className="px-4 py-3">
<p
className={cn(
"text-xs leading-relaxed",
isError ? "text-red-600 dark:text-red-400" : "text-amber-600 dark:text-amber-400"
)}
>
{message}
</p>
</div>
{/* Code frame */}
{frame && (
<div className="mx-4 mb-3 overflow-hidden rounded-xl border border-border bg-muted/40">
<div className="flex items-center justify-between border-b border-border px-3 py-2">
<span className="font-mono text-[11px] text-muted-foreground">{frame.file}</span>
{onOpenFrame && (
<button
type="button"
onClick={() => onOpenFrame(frame.file)}
aria-label="Open in editor"
className="rounded p-0.5 text-muted-foreground transition-colors hover:text-foreground"
>
<ExternalLink className="size-3.5" aria-hidden />
</button>
)}
</div>
<div className="overflow-x-auto p-2 font-mono text-[11px] leading-5">
{frame.lines.map((line) => (
<div
key={line.number}
className={cn(
"flex gap-3 rounded px-1",
line.isError
? "bg-red-50 dark:bg-red-950/40"
: ""
)}
>
<span
className={cn(
"w-6 shrink-0 select-none text-right tabular-nums",
line.isError
? "text-red-400 dark:text-red-500"
: "text-muted-foreground/50"
)}
>
{line.isError ? ">" : ""}{line.number}
</span>
<span className="whitespace-pre text-muted-foreground">
<CodeContent content={line.content} isError={line.isError} />
</span>
</div>
))}
</div>
</div>
)}
{/* Call stack */}
{stack.length > 0 && (
<div className="mx-4 mb-3 overflow-hidden rounded-xl border border-border">
<button
type="button"
onClick={() => setStackOpen((v) => !v)}
aria-expanded={stackOpen}
className="flex w-full items-center justify-between px-3 py-2 text-left transition-colors hover:bg-muted/50"
>
<div className="flex items-center gap-2">
<span className="text-[11px] font-semibold text-foreground">Call Stack</span>
<span className="rounded-md bg-muted px-1.5 py-0.5 font-mono text-[11px] text-muted-foreground tabular-nums">
{stack.length}
</span>
</div>
<ChevronDown
className={cn(
"size-4 shrink-0 text-muted-foreground transition-transform",
stackOpen && "rotate-180"
)}
aria-hidden
/>
</button>
<AnimatePresence initial={false}>
{stackOpen && (
<motion.div
initial={{ height: 0, opacity: 0 }}
animate={{ height: "auto", opacity: 1 }}
exit={{ height: 0, opacity: 0 }}
transition={{ duration: 0.18, ease: "easeInOut" }}
className="overflow-hidden border-t border-border"
>
<div className="divide-y divide-border">
{stack.map((frame, i) => (
<div key={i} className="px-3 py-2">
<p className="text-[11px] font-medium text-foreground">{frame.name}</p>
{frame.context && (
<p className="text-[11px] text-muted-foreground">{frame.context}</p>
)}
{frame.file && (
<p className="mt-0.5 font-mono text-[11px] text-muted-foreground/70">{frame.file}</p>
)}
</div>
))}
</div>
</motion.div>
)}
</AnimatePresence>
</div>
)}
{/* Footer */}
{onHelpful && (
<div className="flex items-center justify-end gap-2 border-t border-border px-4 py-2.5">
<span className="text-[11px] text-muted-foreground">Was this helpful?</span>
<button
type="button"
onClick={() => handleHelpful(true)}
aria-label="Yes, helpful"
aria-pressed={helpfulVote === true}
className={cn(
"rounded-md p-1 transition-colors",
helpfulVote === true
? "text-emerald-500"
: "text-muted-foreground hover:text-foreground"
)}
>
<ThumbsUp className="size-3.5" aria-hidden />
</button>
<button
type="button"
onClick={() => handleHelpful(false)}
aria-label="Not helpful"
aria-pressed={helpfulVote === false}
className={cn(
"rounded-md p-1 transition-colors",
helpfulVote === false
? "text-red-500"
: "text-muted-foreground hover:text-foreground"
)}
>
<ThumbsDown className="size-3.5" aria-hidden />
</button>
</div>
)}
</div>
);
}
function CodeContent({ content, isError }: { content: string; isError?: boolean }) {
return (
<span className={isError ? "text-foreground" : undefined}>
{content}
</span>
);
}
A UX spec for this pattern — written for agents implementing or reusing it, not the code.
# Console Error Card
## Summary
A structured card that surfaces a browser console error or warning inside an AI product's interface. Shows a human-readable error message, an inline code frame pointing to the offending line, a collapsible call stack, and optional navigation across multiple errors — letting the user inspect, copy, and dismiss without leaving the conversation.
## When to use
- When an AI coding assistant detects a runtime error in the user's running application and needs to show it contextually alongside the conversation.
- When surfacing errors collected from a browser session, a test run, or a build log that the AI is helping the user debug.
- When there are multiple errors to navigate through (1/N pattern) and the user needs to triage them one at a time.
## When not to use
- Quota or rate-limit errors — use the Rate Limit pattern instead.
- Simple one-line error toasts that don't need code context — use a standard alert or toast.
- Full-screen error pages (unrecoverable crashes) — this card is for inline, dismissible inspection, not for blocking states.
- Compiler or build errors without a known line number — omit the code frame; don't show an empty frame.
## Anatomy
- **Top bar**: navigation arrows (previous / next) and a "1/N" counter for multi-error sessions; a severity badge ("Console Error" in red, "Console Warning" in amber); copy and close action buttons.
- **Error message**: the raw error text, rendered in the severity color. Should be scannable at a glance.
- **Code frame**: file path and line:col reference in the header; a small window of source lines with the error line highlighted and a ">" gutter indicator; an optional "Open in editor" affordance.
- **Call Stack**: a collapsible section with a frame count badge. Each frame shows the function name, an optional anonymous-context label, and the file path. Hidden by default to reduce visual weight.
- **Helpful footer**: thumbs-up / thumbs-down feedback, revealed only when `onHelpful` is wired up. Lets the product collect signal on error surface quality.
## Behavior
- Navigation arrows are disabled when at the first or last error; they are hidden entirely when `total` is 1.
- The call stack collapses with a height animation (Motion `AnimatePresence`). Opening it does not shift the surrounding layout unexpectedly — use overflow-hidden during transition.
- Copying triggers a brief checkmark-swap animation on the copy icon (≈1.5 s), then reverts. No toast is needed — the icon change is the confirmation.
- The helpful-vote buttons toggle `aria-pressed` and apply a color accent on selection; voting again on the same choice has no effect (idempotent).
- Closing fires `onClose` and is the caller's responsibility — the card itself does not unmount; wrap it in `AnimatePresence` if you need an exit animation.
## Code frame guidelines
- Show 3–6 lines of context around the error line; 2 lines before and 2 after is a good default.
- Always include the line number and column in the file header (`path/to/file.tsx (line:col) @ FunctionName`).
- Highlight only the single error line — highlighting a range suggests a range selection, which is misleading.
- If the source is not available, omit the `frame` prop entirely rather than showing a placeholder.
## Content guidelines
- Error message: verbatim from the console, no paraphrasing. The stack trace may rephrase, but the top-level message must be exact so the user can search for it.
- Badge label: "Console Error" or "Console Warning" — not "Runtime Error", "JS Error", or anything branded.
- Call stack frame names: use the function/component name as it appears in the source, not the mangled bundler name. If the frame is anonymous, show `<anonymous>` literally.
- "Was this helpful?" — only show this when the AI generated the error diagnosis. Don't ask for helpfulness on raw errors the AI simply forwarded.
## Accessibility
- The card root should have `role="alert"` when it appears dynamically so screen readers announce it immediately.
- Navigation buttons have `aria-label="Previous error"` / `aria-label="Next error"`.
- The call-stack toggle button has `aria-expanded` tracking the open state.
- Thumbs buttons have `aria-label` ("Yes, helpful" / "Not helpful") and `aria-pressed` for toggle semantics.
- The code frame is `aria-hidden` to screen readers if the full error message already conveys the problem — avoid reading out raw code lines.
## Related patterns
- Rate Limit — for quota-based errors, not runtime errors.
- Partial Response — for responses cut short by token or context limits.
- Terminal Stream — for streaming build/test output where errors surface in line.