/docs/Development/View Components
back to app →

View components API

Custom views can use the same components the artifuncs app is built from — the ones showcased on the kitchen-sink page — through the af-* elements. They're registered by the host page, so there's nothing to import or install: write the tag, and the real component renders, fully styled and theme-aware in light/dark.

tsx
import { useState } from "react"

export default function View() {
  const [range, setRange] = useState([40, 50])
  return (
    <div className="grid gap-4 p-4">
      <af-slider label="Height range" value={range} onChange={(e: any) => setRange(e.detail[0])} />
      <af-button label="Run" variant="primary" onClick={go} />
    </div>
  )
}

The value / change contract

Every input element takes its state through the value property and reports changes via a change event with the new value in e.detail[0]:

tsx
<af-text-field label="Name" value={name} onChange={(e: any) => setName(e.detail[0])} />

Text controls (af-text-field, af-textarea) additionally emit input on every keystroke with the same payload. af-button needs no bridge — its click is a normal DOM click (onClick). Complex values (arrays, objects) work because JSX sets DOM properties: value={[40, 50]} passes the tuple as-is.

Elements

af-button

tsx
<af-button label="Run" variant="primary" onClick={go} />
<af-button label="Cancel" variant="outline" size="sm" />
<af-button label="Delete" variant="danger" disabled={busy} />
<af-button label="Working…" variant="primary" loading={busy} />
PropValues
labelbutton text
variantprimary (default), secondary, outline, ghost, link, danger, danger-outline, success, warn
sizexs, sm, md (default), lg
loading, disabled, icon-onlybooleans

af-slider

tsx
<af-slider label="Volume" value={vol} min={0} max={100} step={1} unit="%"
  onChange={(e: any) => setVol(e.detail[0])} />

Range mode: pass a tuple — value={[40, 50]} — and you get two draggable thumbs; the change payload stays an ordered [lo, hi] pair (thumbs can't cross).

Props: label, min (0), max (100), step (1, floats OK), unit (readout suffix), hint, disabled.

af-text-field

tsx
<af-text-field label="API key" placeholder="sk-…" value={key}
  onInput={(e: any) => setKey(e.detail[0])} />

Props: label, placeholder, hint, type (e.g. "password"), disabled, readonly, error. Events: input (per keystroke), change.

af-textarea

tsx
<af-textarea label="Prompt" value={prompt} onInput={(e: any) => setPrompt(e.detail[0])} />

Props: label, placeholder, hint, disabled, readonly, error.

af-select

tsx
<af-select label="Mode" items={["fast", "thorough"]} value={mode}
  onChange={(e: any) => setMode(e.detail[0])} />

items accepts an array property (objects too — set item-title / item-value for the display/value keys) or a JSON string attribute. Props: label, placeholder, hint, disabled.

af-checkbox

tsx
<af-checkbox label="Enable feature" value={on} onChange={(e: any) => setOn(e.detail[0])} />

Props: label, hint, disabled, readonly. Value: boolean.

af-date

tsx
<af-date label="Due date" value={date} onChange={(e: any) => setDate(e.detail[0])} />

Value in and out is a YYYY-MM-DD string — JSON-friendly for run(). Props: label, hint, disabled, readonly, clearable.

af-color / af-color-palette

tsx
<af-color label="Accent" value={hex} onChange={(e: any) => setHex(e.detail[0])} />
<af-color-palette label="Palette" value={colors} onChange={(e: any) => setColors(e.detail[0])} />

af-color holds one hex string (show-swatches for preset swatches; show-text-field defaults on; variant="minimal" renders just the rounded-square swatch — no hex text). af-color-palette holds a hex string[].

af-multi-select

tsx
<af-multi-select label="Formats" items={["png", "jpg", "webp"]} value={formats}
  onChange={(e: any) => setFormats(e.detail[0])} />

Like af-select but the value is an array. Same items handling (item-title/item-value for object items).

af-tags-input

tsx
<af-tags-input label="Tags" value={tags} onChange={(e: any) => setTags(e.detail[0])} />

Free-text tag entry; value is a string[].

af-file-field

tsx
const [file, setFile] = useState<File | null>(null)

<af-file-field label="Upload" accept=".pdf,.png" onChange={(e: any) => setFile(e.detail[0])} />
<af-button label="Process" onClick={async () => setOut(await run({ upload: file }))} />

The value is a browser File (or File[] with multiple). Two ways to get it to process():

  1. Just pass it to run() — any File in the input is uploaded automatically and replaced with its filename, so process() receives input["upload"] as a filename string (identical to declared file fields).
  2. Eager uploadconst { name } = await files.upload(file, { onProgress }) uploads immediately (e.g. on select, with a progress bar) and gives you the filename to pass in a later run().

Props: label, hint, accept, multiple, max-size (bytes), max-files, disabled.

Reading files process() wrote (see custom views for the full files API): files.text(name), files.blob(name), files.url(name) (object URL — drop into <img src>), files.save(name) (browser download).

Display elements — af-code-block, af-tag, af-timeline

tsx
<af-code-block code={result.json} language="json" filename="result.json" />
<af-tag label="nlp" />
<af-timeline steps={[
  { state: "done", title: "Fetched" },
  { state: "active", title: "Processing" },
  { state: "pending", title: "Render" },
]} />

af-code-block: code, language, filename, show-copy, show-header. af-tag: label, hash. af-timeline: steps array of { state: 'done'|'active'|'pending'|'failed', title, body?, substeps?, collapsible?, defaultOpen? }.

TypeScript note

The af-* tags aren't in preact's JSX types — either type the event handlers' arg as any (as above) or add a one-liner to your view:

ts
declare module "react" { namespace JSX { interface IntrinsicElements { [tag: `af-${string}`]: any } } }

Layout & custom styling

Use Tailwind utilities (grid, gap-4, p-4, bg-surface, text-ink, …) for layout and anything bespoke, and theme tokens (var(--brand), var(--surface), …) in raw CSS. The af-* elements render in the page's light DOM, so they compose with both.

The live reference for how each component looks in every state is the app's kitchen-sink page (/kitchen-sink when signed in).