# Artifact Card

> Card for something an agent made — document, code, web page, image, sheet or diagram — with generating/updating progress, versions and open-in-panel.

- **Slug:** `artifact-card` · **Category:** conversation · **Status:** beta
- **Import:** `import { ArtifactCard, ArtifactKind, ArtifactStatus } from "@kura-ui/react/artifact-card";`
- **Docs:** https://kuro.abstergo.fyi/components/artifact-card
- **Registry item:** https://kuro.abstergo.fyi/r/artifact-card.json
- **Tags:** artifact, canvas, document, file, output, generation, versions, preview

## Install

```bash
# shadcn registry (copies source into your project)
npx shadcn@latest add https://kuro.abstergo.fyi/r/artifact-card.json
# Kura CLI (same, plus dependency resolution and --json output)
npx @kura-ui/cli add artifact-card
# npm package
npm install @kura-ui/react
```

## When to use

- An assistant message produced a standalone deliverable (a document, a code file, a web page, an image, a CSV, a diagram) that the user opens in a side panel or another page.
- Show an artifact while the agent is still writing it: status="generating" with a live statusText and optional progress, then flip to "ready".
- An artifact the user asked to change: status="updating" plus a ‹ v2/3 › version switcher (versions/version/onVersionChange).
- A list of everything the agent made in a session, e.g. a files/outputs sidebar (compact variant, selected marks the one that is open).
- A visual card with a thumbnail of the artifact under the title (variant="preview" with a `preview` node: a text excerpt, an <img>, a headerless CodeBlock).

## When not to use

- Code the user should read inline in the answer — use code-block directly.
- A file the user uploaded or attached to the prompt — use attachment.
- One tool invocation with arguments and a result (read_file, run_command) — use tool-call.
- A link to an external web source the answer relies on — use citation.
- A multi-step plan of work — use agent-timeline.

## Props

| Prop | Type | Default | Required | Description |
| --- | --- | --- | --- | --- |
| `title` | `string` | — | yes | The artifact's name, e.g. "Q3 launch plan" or "pricing-table.tsx". Truncates. |
| `kind` | `"document" \| "code" \| "web" \| "image" \| "data" \| "diagram" \| "file"` | `"document"` | no | What the agent made. Picks the icon, the spoken label (Document, Code, Web page, Image, Spreadsheet, Diagram, File) and the preview placeholder. |
| `status` | `"generating" \| "updating" \| "ready" \| "error"` | `"ready"` | no | Lifecycle. generating = first write and updating = an edit (both: jade icon tile, shimmering statusText, progress bar, aria-busy); error = danger tile and message. |
| `meta` | `React.ReactNode` | — | no | Secondary line when ready, e.g. "Markdown · 1,240 words" or "TypeScript · 94 lines". Default: the kind label. |
| `statusText` | `React.ReactNode` | — | no | Live line while busy, e.g. "Writing section 3 of 5…". Default "Generating…" / "Updating…". Not announced on every change. |
| `error` | `React.ReactNode` | — | no | Message for status="error". Default "Couldn't finish". |
| `progress` | `number` | — | no | Completion 0–1 while busy: a determinate bar plus a mono percentage. Omit for an indeterminate sweep. |
| `icon` | `React.ReactNode` | — | no | Replaces the kind icon (lucide). |
| `variant` | `"compact" \| "preview"` | `"compact"` | no | compact = one-row chip sized to its content, for message text and lists; preview = full-width card with a 144px thumbnail area above the title. |
| `preview` | `React.ReactNode` | — | no | Thumbnail content for variant="preview": a text excerpt, an <img className="size-full object-cover">, a headerless CodeBlock. It fades out at the bottom and is aria-hidden/inert. Default: a sketch of the kind that shimmers while busy. |
| `versions` | `number` | `1` | no | Number of versions. Above 1 shows a ‹ v2/3 › switcher (disabled while busy). |
| `version` | `number` | — | no | Shown version, 1-based (controlled). |
| `defaultVersion` | `number` | — | no | Initial version when uncontrolled. Default: the latest (`versions`). |
| `onVersionChange` | `(version: number) => void` | — | no | Called with the new 1-based version from the switcher. |
| `onOpen` | `() => void` | — | no | Open the artifact, e.g. into a side panel. The title becomes a button stretched over the whole card. |
| `href` | `string` | — | no | Open by navigating instead: the title becomes a link stretched over the card. |
| `selected` | `boolean` | `false` | no | This artifact is the one currently open (e.g. in the side panel): a stronger ring and aria-current on the open control. |
| `onRetry` | `() => void` | — | no | Shows a Retry icon button when status="error". |
| `actions` | `React.ReactNode` | — | no | Extra controls at the end of the row, e.g. Copy / Download IconButtons with size="sm". They sit above the stretched open target. |

Native attributes of the root element, `className` and `ref` are passed through.

## Anatomy

- **ArtifactCard** (`data-slot="artifact-card"`) — Root. `data-kind`, `data-status`, `data-variant`, `data-selected`; `aria-busy` while generating/updating.
- **Preview** (`data-slot="artifact-card-preview"`) — Thumbnail area (variant="preview" only), aria-hidden and inert.
- **Header** (`data-slot="artifact-card-header"`) — Row with icon, title, meta, versions and actions.
- **Icon** (`data-slot="artifact-card-icon"`) — 36px kind tile; jade while busy, danger on error.
- **Open** (`data-slot="artifact-card-open"`) — The title as a button (onOpen) or link (href) whose ::after covers the card. Without either, the title renders as `artifact-card-title`.
- **Meta** (`data-slot="artifact-card-meta"`) — Second line: meta, shimmering statusText + percentage, or the error.
- **Versions** (`data-slot="artifact-card-versions"`) — ‹ v2/3 › switcher, read as “Version 2 of 3”.
- **Actions** (`data-slot="artifact-card-actions"`) — Retry (on error) and your `actions`.
- **Progress** (`data-slot="artifact-card-progress"`) — 2px role=progressbar at the bottom edge (of the preview, or of the compact card) while busy.

## Accessibility

- The open control is a real button or link named “<title>, <kind>[, generating | , updating | , failed]”; its ::after stretches over the card so the whole card is clickable while Versions and Actions stay separate tab stops.
- `selected` sets aria-current on the open control.
- The preview thumbnail is decorative (aria-hidden + inert): put anything a screen-reader user needs into title/meta.
- A polite status region announces lifecycle changes (“Generating Pricing page”, “Pricing page ready”, “… failed”); statusText and progress ticks are not announced.
- While busy the root has aria-busy and the bar is a named role=progressbar (with aria-valuenow when `progress` is given).
- Reduced motion: The shimmer and the indeterminate sweep stop (a still tinted track remains); the hover zoom of the preview, the press scale and the chevron nudge are removed. Colour and opacity changes stay.

| Keys | Action |
| --- | --- |
| Tab | Open control, then the version arrows, Retry and your actions |
| Enter / Space | Open the artifact (Enter follows the link for href) |

## Notes for agents

- Keep the same card mounted from generating → ready so the transition (and the single announcement) is smooth; switch `status`, don't swap components.
- Drive `selected` from the id of the artifact open in your panel, so the card the user is looking at is marked.
- Use variant="compact" inside message text and lists (it sizes to content — give it a width like w-full or max-w-sm when stacking); use "preview" when there is room for a thumbnail.
- For images pass preview={<img src={url} alt="" className="size-full object-cover" />} — the thumbnail is decorative, the title carries the name.
- Pass `progress` only when you really know it (e.g. sections written / planned); otherwise leave it out for the indeterminate sweep.
- Map Anthropic/OpenAI canvas or artifact tool calls (create → generating, update/rewrite → updating with versions + 1) onto one card per artifact id.

## Examples

### Compact card (minimal)

Title, meta and an open action that toggles `selected`.

```tsx
"use client";

import * as React from "react";
import { ArtifactCard } from "@kura-ui/react/artifact-card";

export function ArtifactCardMinimal() {
  const [open, setOpen] = React.useState(false);
  return (
    <ArtifactCard
      title="Q3 launch plan"
      kind="document"
      meta="Markdown · 1,240 words"
      selected={open}
      onOpen={() => setOpen((o) => !o)}
      className="w-80"
    />
  );
}
```

### Written live, then edited (full)

Preview variant generating with progress and statusText, then ready with versions, Copy/Download actions and an edit that creates v2.

```tsx
"use client";

import { Copy, Download, RotateCcw } from "lucide-react";
import * as React from "react";
import { Button } from "@kura-ui/react/button";
import { IconButton } from "@kura-ui/react/icon-button";
import { ArtifactCard } from "@kura-ui/react/artifact-card";

const PLAN = [
  "Pricing page rewrite",
  "Lead with the outcome, not the plan names. Most visitors arrive from a comparison search and decide in the first screen.",
  "Collapse the four tiers into three. Team and Business overlap on every feature that matters to a buyer under fifty seats.",
  "Move the FAQ above the fold on mobile and answer the two questions sales hears most: data residency and seat changes.",
];

const STEPS = ["Outlining…", "Writing the summary…", "Writing section 2 of 3…", "Polishing…"];

export function ArtifactCardFull() {
  const [run, setRun] = React.useState(0);
  const [progress, setProgress] = React.useState(0);
  const [version, setVersion] = React.useState(1);
  const [open, setOpen] = React.useState(false);
  const busy = progress < 1;

  // biome-ignore lint/correctness/useExhaustiveDependencies: `run` restarts the simulation
  React.useEffect(() => {
    setProgress(0);
    const id = setInterval(() => {
      setProgress((p) => {
        const next = Math.min(1, p + 0.035 + Math.random() * 0.03);
        if (next >= 1) clearInterval(id);
        return next;
      });
    }, 140);
    return () => clearInterval(id);
  }, [run]);

  const visible = busy ? Math.max(1, Math.ceil(progress * PLAN.length)) : PLAN.length;
  const versions = run + 1;

  return (
    <div className="flex w-full max-w-sm flex-col gap-3">
      <ArtifactCard
        variant="preview"
        kind="document"
        title="Pricing page rewrite"
        status={busy ? (run === 0 ? "generating" : "updating") : "ready"}
        statusText={STEPS[Math.min(STEPS.length - 1, Math.floor(progress * STEPS.length))]}
        progress={progress}
        meta={`Markdown · ${version === versions ? "412" : "386"} words · edited just now`}
        versions={busy ? Math.max(1, versions - 1) : versions}
        version={busy ? Math.max(1, versions - 1) : version}
        onVersionChange={setVersion}
        selected={open}
        onOpen={() => setOpen((o) => !o)}
        actions={
          <>
            <IconButton size="sm" label="Copy" icon={<Copy />} disabled={busy} />
            <IconButton size="sm" label="Download" icon={<Download />} disabled={busy} />
          </>
        }
        preview={
          <div className="flex flex-col gap-2 p-5 text-[11px] text-kura-fg-muted leading-relaxed">
            {PLAN.slice(0, visible).map((line, i) =>
              i === 0 ? (
                <p key={line} className="font-semibold text-[13px] text-kura-fg tracking-tight">
                  {line}
                </p>
              ) : (
                <p key={line} className="animate-kura-fade-in">
                  {line}
                </p>
              ),
            )}
          </div>
        }
      />
      <div className="flex items-center justify-between text-kura-fg-subtle text-xs">
        <span>{open ? "Open in the side panel" : "Click the card to open it"}</span>
        <Button
          size="xs"
          variant="ghost"
          disabled={busy}
          onClick={() => {
            setRun((r) => r + 1);
            setVersion(run + 2);
          }}
        >
          <RotateCcw />
          Ask for an edit
        </Button>
      </div>
    </div>
  );
}
```

### Every status (state)

Generating (indeterminate), updating with progress and versions, ready, failed with Retry.

```tsx
"use client";

import { ArtifactCard } from "@kura-ui/react/artifact-card";

export function ArtifactCardStates() {
  return (
    <div className="flex w-full max-w-sm flex-col gap-2.5">
      <ArtifactCard
        title="Onboarding email sequence"
        status="generating"
        statusText="Drafting email 2 of 4…"
        onOpen={() => {}}
        className="w-full"
      />
      <ArtifactCard
        kind="code"
        title="use-session.ts"
        status="updating"
        progress={0.64}
        statusText="Applying 3 edits…"
        versions={2}
        onOpen={() => {}}
        className="w-full"
      />
      <ArtifactCard
        kind="data"
        title="Churn by cohort"
        meta="CSV · 12 columns · 4,218 rows"
        onOpen={() => {}}
        className="w-full"
      />
      <ArtifactCard
        kind="web"
        title="Waitlist landing page"
        status="error"
        error="Stopped: the response hit the output limit"
        onRetry={() => {}}
        onOpen={() => {}}
        className="w-full"
      />
    </div>
  );
}
```

### Every kind (variant)

Document, code, web page, image, spreadsheet and diagram.

```tsx
"use client";

import * as React from "react";
import { ArtifactCard } from "@kura-ui/react/artifact-card";

const KINDS = [
  { kind: "document", title: "Board update — October", meta: "Markdown · 860 words" },
  { kind: "code", title: "rate-limiter.ts", meta: "TypeScript · 94 lines" },
  { kind: "web", title: "Pricing page", meta: "React · interactive" },
  { kind: "image", title: "Hero illustration", meta: "PNG · 1600 × 900" },
  { kind: "data", title: "Q3 pipeline", meta: "CSV · 1,204 rows" },
  { kind: "diagram", title: "Auth flow", meta: "Mermaid · 9 nodes" },
] as const;

export function ArtifactCardKinds() {
  return (
    <div className="grid w-full max-w-xl grid-cols-1 gap-2.5 sm:grid-cols-2">
      {KINDS.map((a) => (
        <ArtifactCard key={a.kind} {...a} onOpen={() => {}} className="w-full" />
      ))}
    </div>
  );
}
```

### In a message, opening a side panel (composition)

An assistant Message with a code artifact whose preview is a headerless CodeBlock; the card toggles a side panel with the full file.

```tsx
"use client";

import { X } from "lucide-react";
import * as React from "react";
import { CodeBlock } from "@kura-ui/react/code-block";
import { IconButton } from "@kura-ui/react/icon-button";
import { Message } from "@kura-ui/react/message";
import { ArtifactCard } from "@kura-ui/react/artifact-card";

const CODE = `import { Ratelimit } from "@upstash/ratelimit";
import { kv } from "@vercel/kv";

// 20 requests per 10 seconds, per user, sliding window.
export const limiter = new Ratelimit({
  redis: kv,
  limiter: Ratelimit.slidingWindow(20, "10 s"),
  prefix: "chat",
});

export async function guard(userId: string) {
  const { success, reset } = await limiter.limit(userId);
  if (!success) {
    const retry = Math.ceil((reset - Date.now()) / 1000);
    throw new Response("Slow down", {
      status: 429,
      headers: { "Retry-After": String(retry) },
    });
  }
}`;

export function ArtifactCardComposition() {
  const [open, setOpen] = React.useState(true);
  return (
    <div className="flex w-full max-w-3xl items-start gap-4">
      <div className="flex min-w-0 flex-1 flex-col gap-4">
        <Message from="user">Rate limit the chat route to 20 requests per 10 seconds.</Message>
        <Message from="assistant" name="Kura">
          <p>
            Here's a sliding-window limiter keyed by user. Call <code>guard()</code> at the top of
            the route; it throws a 429 with <code>Retry-After</code> when someone is over.
          </p>
          <ArtifactCard
            variant="preview"
            kind="code"
            title="rate-limiter.ts"
            meta="TypeScript · 21 lines"
            selected={open}
            onOpen={() => setOpen((o) => !o)}
            className="max-w-xs"
            preview={
              <CodeBlock
                code={CODE}
                language="ts"
                header={false}
                lineNumbers={false}
                copyable={false}
                className="rounded-none bg-transparent text-[11px] shadow-none"
              />
            }
          />
        </Message>
      </div>
      {open && (
        <section
          aria-label="rate-limiter.ts"
          className="hidden w-[22rem] shrink-0 animate-kura-fade-in flex-col overflow-hidden rounded-kura-xl bg-kura-surface shadow-kura-md md:flex"
        >
          <header className="flex h-11 items-center gap-2 pr-1.5 pl-3.5 shadow-[inset_0_-1px_0_var(--kura-border-subtle)]">
            <span className="min-w-0 flex-1 truncate font-medium text-sm">rate-limiter.ts</span>
            <IconButton size="sm" label="Close panel" icon={<X />} onClick={() => setOpen(false)} />
          </header>
          <CodeBlock
            code={CODE}
            language="ts"
            header={false}
            maxHeight={320}
            className="rounded-none text-xs shadow-none"
          />
        </section>
      )}
    </div>
  );
}
```

## Dependencies

- npm: `lucide-react`
- Kura registry items: `kura-theme`, `icon-button`, `shimmer-text`, `use-controllable-state`, `utils`
- Related: `message`, `code-block`, `attachment`, `tool-call`, `streaming-text`
