Local-First Artifacts: A Docked Canvas Driven by an In-Browser Model
@localmode/ui adds the Artifacts family -- a Claude-style docked canvas (Artifact), a sortable DataTableArtifact, a dependency-free ChartArtifact, and a CodeDiffViewer. All four render generated output beside the chat, driven entirely by a model running in the browser. No server, no sandbox, no remote execution.
When a model generates a whole file, a table of records, a chart, or a rewritten paragraph, cramming it into a chat bubble is the wrong shape. Every modern AI UI library solves this with an artifact: a docked canvas that renders generated output beside the conversation, where the user can copy it, download it, sort it, or regenerate it.
An audit of eight external AI UI libraries found that every one of them ships an artifact surface -- and @localmode/ui had none. It was the single biggest structural gap. The Artifacts family closes it with four copy-owned, shadcn-styled primitives, and they are fully local-first: a model running in the browser drives the content, and the shell never touches a server.
Local content only
The artifact shell is purely presentational. A local model -- via useGenerateText() /
useGenerateObject() -- supplies the content. No server, no sandbox, no remote execution.
Install
With shadcn/ui set up and the @localmode namespace mapped in components.json:
# One primitive
npx shadcn@latest add @localmode/ui/artifacts/artifact
# All four artifact primitives
npx shadcn@latest add @localmode/ui/artifactsThe command copies the .tsx into your project -- no @localmode/* package is installed; pair them with the recommended @localmode/react hooks, or any backend. ChartArtifact and CodeDiffViewer add no charting or diff library -- the rendering is in-component.
Artifact: a Claude-style docked canvas
Artifact is the canvas shell: a vertical surface with a header (title + description), an action toolbar (copy / download / refresh / close), and a scrollable content region. It renders generated output -- code, a markdown doc, an SVG, raw HTML -- beside the chat instead of inline.
It composes from sub-parts: Artifact (root), ArtifactHeader, ArtifactTitle, ArtifactDescription, ArtifactActions, ArtifactAction, ArtifactClose, and ArtifactContent. The toolbar behaviors are all client-side: ArtifactAction with content only copies to the clipboard, content + fileName downloads a real Blob, and onClick runs your callback (e.g. re-run generation).
import { useGenerateText } from '@localmode/react';
import { transformers } from '@localmode/transformers';
import {
Artifact,
ArtifactHeader,
ArtifactTitle,
ArtifactDescription,
ArtifactActions,
ArtifactAction,
ArtifactClose,
ArtifactContent,
} from '@/components/artifacts/artifact';
export function GeneratedCodeCanvas({ close }) {
const model = transformers.languageModel('onnx-community/granite-4.0-350m-ONNX-web');
const { data, isLoading, execute } = useGenerateText({ model, maxTokens: 200 });
const code = data?.text ?? '';
return (
<Artifact className="h-96 w-[28rem]">
<ArtifactHeader>
<div>
<ArtifactTitle>generated.ts</ArtifactTitle>
<ArtifactDescription>From a local model run</ArtifactDescription>
</div>
<ArtifactActions>
<ArtifactAction content={code} label="Copy" />
<ArtifactAction content={code} fileName="generated.ts" label="Download" />
<ArtifactAction onClick={() => execute('Write a slugify function')} label="Refresh" />
<ArtifactClose onClick={close} />
</ArtifactActions>
</ArtifactHeader>
<ArtifactContent>
{isLoading ? <p>Generating...</p> : <pre>{code}</pre>}
</ArtifactContent>
</Artifact>
);
}Refresh re-runs the model on the user's device. Download produces a Blob in the browser. Nothing leaves the machine.
DataTableArtifact: a sortable table for local rows
DataTableArtifact is a docked, sortable table for any local row data -- VectorDB search results, a model catalog, evaluation rows, or the array output of generateObject(). Click a column header to sort ascending -> descending -> unsorted, all computed in-browser. Sorting reorders a copy of your rows; the input array is untouched.
import { useGenerateObject, jsonSchema } from '@localmode/react';
import { z } from 'zod';
import { DataTableArtifact } from '@/components/artifacts/data-table-artifact';
const schema = jsonSchema(
z.object({ records: z.array(z.object({ name: z.string(), score: z.number() })) })
);
export function RecordsTable({ model }) {
const { data } = useGenerateObject({ model, schema });
return (
<DataTableArtifact
rows={data?.records ?? []}
columns={[
{ key: 'name', header: 'Name' },
{ key: 'score', header: 'Score' },
]}
/>
);
}It is distinct from the inline ScoredResultBarList -- this is a docked canvas you scan and re-sort, not a message-stream atom.
ChartArtifact: dependency-free local data-viz
ChartArtifact renders six chart types -- line, bar, area, scatter, radar, gauge -- with minimal inline SVG and no charting library. It targets genuinely on-device metrics:
- radar -- evaluation curves (precision / recall / F1 / accuracy) from
useEvaluateModel(). - scatter -- 2D embedding projections (PCA / UMAP).
- line / area -- drift-over-time from
useReindex(), latency or tokens-per-second trends. - bar -- embedding-similarity distributions, per-class counts.
- gauge -- a single normalized score, e.g. overall F1.
import { ChartArtifact } from '@/components/artifacts/chart-artifact';
<ChartArtifact
type="radar"
data={[
{ label: 'Precision', value: 0.91 },
{ label: 'Recall', value: 0.87 },
{ label: 'F1', value: 0.89 },
{ label: 'Accuracy', value: 0.93 },
]}
/>Because colors come from currentColor plus shadcn/ui tokens, every chart inherits your theme. It is not a generic BI widget -- its value is visualizing exactly what runs in the browser.
CodeDiffViewer: see what your local pipeline changed
CodeDiffViewer shows a line-level diff of two local strings -- additions in green, deletions in red -- in a unified (single-column) or split (side-by-side) layout. The diff is computed in-browser with a dependency-free longest-common-subsequence algorithm; there is no ML hook and no server. It is the text/code analog of an image before/after viewer.
import { redactPII } from '@localmode/core';
import { CodeDiffViewer } from '@/components/artifacts/code-diff-viewer';
export function RedactionDiff({ raw }) {
const redacted = redactPII(raw);
return <CodeDiffViewer original={raw} modified={redacted} mode="split" />;
}Use it to make any local transform legible: PII redaction (raw -> redacted), translation (source -> target), or prompt and formatting edits.
What LocalMode deliberately leaves out
External artifact catalogs include members that only make sense with a server behind them. LocalMode refuses those because they would reintroduce a cloud dependency:
- Sandbox / Terminal -- remote code execution.
- Web Preview -- a deployed-app iframe.
- Test Results / Stack Trace / Commit / Package Info / File Tree / Env Vars -- CI / VCS / IDE tooling.
A live JSX preview (generative UI -- rendering model-emitted JSX/JSON) is deferred, not refused: it is local-first-capable and high-interest, but rendering arbitrary model-emitted UI carries arbitrary-code-execution risk and needs a safe allowlist/sandbox design first. It is tracked as a future change.
How artifacts fit the rest of @localmode/ui
Artifacts are the output canvas for content generated by the conversation family -- a Response or StructuredOutputViewer produces text or JSON, and a button promotes it into a docked Artifact, DataTableArtifact, or ChartArtifact beside the stream. And the data they render often comes from the local-first family: a VectorStorageObservability metric becomes a ChartArtifact line, a model catalog becomes a DataTableArtifact. The three families are designed to click together into one local-first app.
FAQ
What is an artifact in a chat UI?
An artifact is a docked side-panel or canvas that renders generated output -- code, a document, a table, a chart, a diff -- beside the chat rather than inline. The @localmode/ui Artifacts family ships four: Artifact (a Claude-style canvas shell), DataTableArtifact (a sortable table), ChartArtifact (a dependency-free SVG chart), and CodeDiffViewer (a line-level text diff).
How are @localmode/ui artifacts local-first?
The shell is purely presentational and the content comes from a model running in the browser via useGenerateText or useGenerateObject. There is no server, no sandbox, and no remote execution -- copy writes to the clipboard, download produces a real Blob, refresh re-runs local generation, and charts and diffs are computed in-component from local data.
How do I install the artifacts components?
Use the shadcn CLI: npx shadcn@latest add @localmode/ui/artifacts/artifact for a single primitive, or npx shadcn@latest add @localmode/ui/artifacts for all four. The command copies the .tsx files into your project -- no @localmode/* package is installed. Pair them with the recommended @localmode/react hooks, or any backend.
Do ChartArtifact and CodeDiffViewer require a charting or diff library?
No. ChartArtifact renders six chart types with minimal inline SVG and no charting library, and CodeDiffViewer computes its line-level diff in-component with a dependency-free LCS algorithm. Neither adds an external dependency.
What artifact types does LocalMode deliberately not ship?
LocalMode refuses the server- and IDE-centric members of external catalogs -- Sandbox, Web Preview, Terminal, Test Results, Stack Trace, Commit, Package Info, File Tree, and Env Vars -- because they require remote execution, deploys, or CI/VCS tooling. A live JSX/generative-UI preview is deferred, not refused, pending a safe sandbox design.
Methodology
Every LocalMode-specific claim in this post was verified directly against the repository, which is the source of truth:
- Family count and membership (four artifacts:
Artifact,DataTableArtifact,ChartArtifact,CodeDiffViewer) — confirmed inapps/ui/registry.json(four items under theartifactscategory) and the catalog summary inAGENTS.md("Artifacts & Canvas", 4). - Sub-parts, props, and behaviors — read from the actual component source.
Artifactcomposes fromArtifactHeader,ArtifactTitle,ArtifactDescription,ArtifactActions,ArtifactAction,ArtifactClose, andArtifactContent;ArtifactActioncopies oncontent, downloads oncontent+fileName, and runs your callback ononClick(registry/localmode/artifacts/artifact/artifact.tsx).CodeDiffViewer's real props areoriginal/modified/mode(the prior draft'sbefore/after/viewexample was corrected).DataTableArtifactsorts a copy of the rows ascending → descending → unsorted (data-table-artifact.tsx). - "Dependency-free" chart and diff — verified by inspection:
chart-artifact.tsxrenders six chart types (line,bar,area,scatter,radar,gauge) with inline SVG and no charting import;code-diff-viewer.tsxcomputes a line-level diff with an in-component longest-common-subsequence algorithm. The registry entries for both list zero npmdependencies. - Install footprint ("no
@localmode/*package is installed") — confirmed againstapps/ui/registry.json(the artifacts items declare onlylucide-reactor nothing as npm dependencies; theirregistryDependenciesare copy-owned@localmode/ui/...files, not packages) and the real-shadcn-CLI portability test inapps/ui/consumer-tests/portability-test.mjs, which proves an installed component contains zero@localmode/*import statements and zero@localmode/*packages. - Hooks and helpers in code examples —
useGenerateText,useGenerateObject,jsonSchema,useEvaluateModel, anduseReindexare exported from@localmode/react(packages/react/src/index.ts);redactPIIis exported from@localmode/coreand returns astring(packages/core/src/security/pii.ts);transformers.languageModel('onnx-community/granite-4.0-350m-ONNX-web')is a real catalog model (packages/transformers/src/models.ts). The@/components/artifacts/<component>import path matches the official docs pages underapps/ui/content/docs/artifacts/. - "Audit of eight external AI UI libraries" and the deliberately-excluded members (Sandbox, Web Preview, Terminal, Test Results, Stack Trace, Commit, Package Info, File Tree, Env Vars) plus the deferred generative-UI JSX preview — grounded in the change record
openspec/changes/ui-elements-artifacts/proposal.md(which names all eight: AI Elements, assistant-ui, AI SDK Agents, ElevenLabs UI, HeyGaia, prompt-kit, tool-ui, shadcn.io/ai) anddesign.md. - Cross-family references (
ScoredResultBarList,Response,StructuredOutputViewer,VectorStorageObservability,BeforeAfterImageViewer) — confirmed present inapps/ui/registry.json.
External claims (shadcn CLI / registry mechanics) were checked against the official shadcn/ui documentation linked below.
Sources
- LocalMode artifacts registry entries —
apps/ui/registry.json - LocalMode artifacts component source —
apps/ui/registry/localmode/artifacts/(artifact/artifact.tsx,data-table-artifact/data-table-artifact.tsx,chart-artifact/chart-artifact.tsx,code-diff-viewer/code-diff-viewer.tsx) - LocalMode artifacts docs pages —
apps/ui/content/docs/artifacts/ - Portability test (zero-
@localmode-dependency proof) —apps/ui/consumer-tests/portability-test.mjs @localmode/reacthook exports —packages/react/src/index.tsredactPIIsignature —packages/core/src/security/pii.ts- Transformers model catalog —
packages/transformers/src/models.ts - Artifacts change record (eight-library audit, refused/deferred members) —
openspec/changes/ui-elements-artifacts/{proposal,design,tasks}.md - UI catalog summary —
AGENTS.md - shadcn/ui Installation
- shadcn/ui Registry
Get started
Install all four and dock your first canvas:
npx shadcn@latest add @localmode/ui/artifactsSee all three in action in the Agent & Structured Data block — the artifacts family's home block, where a schema-validated on-device extraction docks into an artifact canvas as a sortable data-table-artifact and a chart-artifact built from the extraction's real numbers (never fixtures).
Browse props, live previews, and examples in the artifacts docs. For the chat surfaces that drive artifact content, see the conversation components post; for the model and device surfaces underneath, the local-first components post.
Frequently Asked Questions
- What is an artifact in a chat UI?
- An artifact is a docked side-panel or canvas that renders generated output -- code, a document, a table, a chart, a diff -- beside the chat rather than inline in the message stream. The @localmode/ui Artifacts family ships four: Artifact (a Claude-style canvas shell), DataTableArtifact (a sortable table), ChartArtifact (a dependency-free SVG chart), and CodeDiffViewer (a line-level text diff).
- How are @localmode/ui artifacts local-first?
- The artifact shell is purely presentational and the content comes from a model running in the browser via useGenerateText or useGenerateObject. There is no server, no sandbox, and no remote execution. Copy writes to the clipboard, download produces a real Blob, refresh re-runs your local generation, and charts and diffs are computed in-component from local data.
- How do I install the artifacts components?
- Use the shadcn CLI. Run npx shadcn@latest add @localmode/ui/artifacts/artifact for a single primitive, or npx shadcn@latest add @localmode/ui/artifacts to install all four. The command copies the .tsx files into your project so you own the code -- no @localmode/* package is installed. Pair them with the recommended @localmode/react hooks, or any backend.
- Do ChartArtifact and CodeDiffViewer require a charting or diff library?
- No. ChartArtifact renders six chart types -- line, bar, area, scatter, radar, gauge -- with minimal inline SVG and no charting library, so the copied component stays small and tree-shakeable. CodeDiffViewer computes a line-level diff in-component with a dependency-free longest-common-subsequence algorithm. Neither adds an external dependency.
- What artifact types does LocalMode deliberately not ship?
- LocalMode refuses the server- and IDE-centric members of external artifact catalogs -- Sandbox, Web Preview, Terminal, Test Results, Stack Trace, Commit, Package Info, File Tree, and Env Vars -- because they require remote code execution, deploys, or CI/VCS tooling and would reintroduce a server dependency. A live JSX/generative-UI preview is deferred, not refused, pending a safe sandbox design.