All components

AI Artifact

AI Artifact is a React and Tailwind CSS component from SmoothUI, licensed MIT. Copy it and paste it into your project.

SmoothUI MIT other

What it is

AI Artifact is a React and Tailwind CSS component from SmoothUI, licensed MIT. Copy it and paste it into your project.

How to use it

Open it in the catalogue, press copy, and paste it into your project. The prompt you copy carries the code and the rules that tell your agent to reproduce it without changing anything.

Open in the catalogue

Five free copies a month. No card.

Needs

npm i lucide-react motion

Licence

This component comes from SmoothUI and is redistributed under the MIT licence, which permits commercial use. The copyright notice travels with the code.

AI Artifact

index.tsx
"use client";

import { cn } from "@/lib/utils";
import { Check, Copy } from "lucide-react";
import { AnimatePresence, motion, useReducedMotion } from "motion/react";
import { type ReactNode, useEffect, useId, useState } from "react";

const SPRING_DEFAULT = {
  bounce: 0.1,
  duration: 0.25,
  type: "spring" as const,
};
const EASE_OUT = [0.23, 1, 0.32, 1] as const;
const TRAVEL_PX = 24;
const COPIED_RESET_MS = 1600;

export type AIArtifactPane = "preview" | "code";

export type AIArtifactProps = {
  className?: string;
  /** The raw form — source, JSON, markup. */
  code?: ReactNode;
  /** Plain text handed to the clipboard. Omit to hide the copy action. */
  copyText?: string;
  /** Which pane to start on. */
  defaultPane?: AIArtifactPane;
  /** The rendered form. */
  preview?: ReactNode;
  /** Name the artifact so it can be referred to in conversation. */
  title: string;
};

const PANES: AIArtifactPane[] = ["preview", "code"];

/**
 * A frame around something the model produced.
 *
 * The two panes travel along a single horizontal axis — preview sits to the left
 * of code, always — so the swap has a direction and, after one go, a memory. A
 * cross-fade between them would leave the user with no sense of where the other
 * pane went, and no expectation of where it will come back from.
 */
const AIArtifact = ({
  className,
  code,
  copyText,
  defaultPane = "preview",
  preview,
  title,
}: AIArtifactProps) => {
  const shouldReduceMotion = useReducedMotion();
  const [pane, setPane] = useState<AIArtifactPane>(defaultPane);
  const [hasCopied, setHasCopied] = useState(false);
  const indicatorId = useId();

  const available = PANES.filter((candidate) =>
    candidate === "preview" ? Boolean(preview) : Boolean(code)
  );

  useEffect(() => {
    if (!hasCopied) {
      return;
    }
    const timeout = setTimeout(() => setHasCopied(false), COPIED_RESET_MS);
    return () => clearTimeout(timeout);
  }, [hasCopied]);

  const copy = async () => {
    if (!copyText) {
      return;
    }
    try {
      await navigator.clipboard.writeText(copyText);
      setHasCopied(true);
    } catch {
      // A blocked clipboard is not worth an error state.
    }
  };

  const direction = pane === "code" ? 1 : -1;

  return (
    <div
      className={cn(
        "w-full overflow-hidden rounded-xl border border-border bg-background",
        className
      )}
    >
      <div className="flex items-center gap-2 border-border border-b px-2 py-1.5">
        <span className="min-w-0 truncate px-1 font-medium text-foreground text-xs">
          {title}
        </span>

        {available.length > 1 && (
          <div className="ml-auto flex items-center gap-0.5 rounded-lg bg-muted p-0.5">
            {available.map((candidate) => (
              <button
                aria-selected={pane === candidate}
                className={cn(
                  "relative cursor-pointer rounded-md px-2 py-1 text-xs capitalize transition-colors",
                  pane === candidate
                    ? "text-foreground"
                    : "text-muted-foreground hover:text-foreground"
                )}
                key={candidate}
                onClick={() => setPane(candidate)}
                role="tab"
                type="button"
              >
                {pane === candidate && (
                  // A single shared element slides between the tabs. Two
                  // separately styled tabs would just swap colour, which reads
                  // as two states rather than one selection moving.
                  <motion.span
                    className="absolute inset-0 rounded-md bg-background shadow-sm"
                    layoutId={`${indicatorId}-indicator`}
                    transition={
                      shouldReduceMotion ? { duration: 0 } : SPRING_DEFAULT
                    }
                  />
                )}
                <span className="relative">{candidate}</span>
              </button>
            ))}
          </div>
        )}

        {copyText ? (
          <button
            aria-label={hasCopied ? "Copied" : "Copy"}
            className={cn(
              "cursor-pointer rounded-md p-1.5 transition-colors",
              available.length > 1 ? "" : "ml-auto",
              hasCopied
                ? "text-foreground"
                : "text-muted-foreground hover:bg-muted hover:text-foreground"
            )}
            onClick={copy}
            type="button"
          >
            {hasCopied ? (
              <Check aria-hidden="true" size={13} />
            ) : (
              <Copy aria-hidden="true" size={13} />
            )}
          </button>
        ) : null}
      </div>

      <div className="relative overflow-hidden">
        <AnimatePresence custom={direction} initial={false} mode="wait">
          <motion.div
            animate={{ opacity: 1, x: 0 }}
            custom={direction}
            exit={
              shouldReduceMotion
                ? { opacity: 0, transition: { duration: 0 } }
                : { opacity: 0, x: -direction * TRAVEL_PX }
            }
            initial={
              shouldReduceMotion
                ? { opacity: 1, x: 0 }
                : { opacity: 0, x: direction * TRAVEL_PX }
            }
            key={pane}
            transition={
              shouldReduceMotion
                ? { duration: 0 }
                : { duration: 0.2, ease: EASE_OUT }
            }
          >
            {pane === "preview" ? (
              <div className="p-3">{preview}</div>
            ) : (
              <pre className="overflow-x-auto p-3 font-mono text-foreground text-xs leading-relaxed">
                {code}
              </pre>
            )}
          </motion.div>
        </AnimatePresence>
      </div>
    </div>
  );
};

export default AIArtifact;

Similar components