/docs/Development/Custom Views
back to app →

Custom views

Declared fields cover most funcs, but sometimes you want to own every pixel. A CUSTOM func replaces fields.input / fields.output with a frontend you write yourself: a view.tsx that renders the whole UI and calls your backend process() like a function.

File layout

text
my-func/
├── artifuncs.json       # "type": "CUSTOM", "view": "view/view.tsx"
├── main.py              # backend — process(input, settings) -> dict
├── requirements.txt     # backend deps (pip)
└── view/
    ├── view.tsx         # the whole frontend UI (entry point)
    └── package.json     # frontend deps (pnpm)

Everything frontend lives under view/; the root stays backend-only.

artifuncs.json

json
{
  "name": "SHA-256",
  "type": "CUSTOM",
  "language": "PYTHON",
  "view": "view/view.tsx"
}

No fields — the view owns the UI. settings still works and is readable from the view.

The view

A default-exported React-style component. Import from "react" (a light Preact runtime under the hood — hooks work as usual) and "@artifuncs/sdk":

tsx
import { useState } from "react"
import { useArtifuncs } from "@artifuncs/sdk"

export default function View() {
  const { run } = useArtifuncs()
  const [text, setText] = useState("")
  const [result, setResult] = useState<{ hash: string } | null>(null)

  return (
    <div className="grid gap-3 p-4">
      <textarea className="rounded border border-rule bg-surface p-2 text-ink"
        value={text} onChange={e => setText(e.target.value)} />
      <button className="rounded bg-brand px-4 py-2 text-brand-ink"
        onClick={async () => setResult(await run({ text }))}>
        Hash it
      </button>
      {result && <code className="rounded bg-surface-2 p-2 font-mono text-ink">{result.hash}</code>}
    </div>
  )
}

The SDK — useArtifuncs()

MemberDescription
run(input)Calls process(input, settings) in the sandbox. Returns a Promise of the exact dict process() returned. Rejects with a typed error (e.type, e.message) when process() raises. Settings are attached automatically — run takes input only.
settingsSettings API: settings.get(key, fallback?) (effective value: view-written > instance > declared default), settings.set(key, value) (visible to the next run(); persists on board instances), settings.all() (snapshot dict), settings.open() (opens the tool's settings panel).
on(event, cb)Subscribe to view events; returns an unsubscribe function. "settings.valueChanged"cb(key, oldValue, newValue) fires when a setting changes (e.g. the user edits the settings panel — the view is NOT remounted; react in the handler). "theme.changed"cb(theme) on light/dark switches.
theme.get(token)Resolved theme token value, e.g. theme.get("brand") → the brand color — for canvas / chart code that needs real values in JS.
onLog(cb)Live log stream: every print() in process() arrives as { level, message, timestamp }. Returns an unsubscribe function.
filesRun-scoped file API — see below.

Multiple backend operations? Branch on a key: run({ action: "hash", text }) and switch in process().

Files — ctx.files

All file operations share the view's run scope, so uploads are visible to process() and everything process() writes is fetchable back:

MemberDescription
files.upload(file, { onProgress }?)Upload a browser File eagerly. Resolves { name, size }; pass name in a later run() input. onProgress(loaded, total) for progress bars.
files.text(name)Read a file process() wrote, as a string.
files.blob(name)Read as a Blob.
files.url(name)Object URL — drop straight into <img src> / <a href>. Auto-revoked when the view unmounts.
files.save(name, saveAs?)Trigger a browser download.

The shortcut: you usually don't need upload() at all — any File object you pass in a run() input is uploaded automatically and replaced with its filename, so process() receives the standard filename string:

tsx
const out = await run({ upload: file })          // file is a File from <af-file-field>
const imgSrc = await files.url(out.result.path)  // show what process() generated

Styling

Tailwind utilities are compiled for your view automatically — no config, no install. Color, radius and font utilities are mapped to the app's theme tokens, so your view matches the app and follows light/dark mode with zero effort:

  • Surfaces: bg-bg, bg-surface, bg-surface-2, bg-surface-3
  • Text: text-ink, text-ink-2, text-ink-3, text-ink-4
  • Brand: bg-brand, text-brand-ink, bg-brand-soft, hover:bg-brand-hover
  • Borders: border-rule, border-rule-2
  • Semantics: bg-info|success|warn|danger (+ -soft backgrounds, text-*-ink text)
  • Radius: rounded-sm, rounded, rounded-lg · Fonts: font-sans, font-mono

Standard Tailwind spacing/layout (p-4, gap-2, grid, flex, text-sm, …) all work. Raw CSS with var(--surface) etc. works too.

For ready-made buttons, inputs, cards, alerts and badges, use the host's component classes — see the view components API.

Your view mounts edge-to-edge — there is no default padding around it. Field-based funcs get a 16px content inset from the host; CUSTOM views don't, so you own every pixel of the tile. Want breathing room? Add it yourself (e.g. a p-4 wrapper, like the starter template). Want a full-bleed canvas, map, or editor? Just don't.

Frontend dependencies

Declare in view/package.json — installed with pnpm and bundled into your view on save:

json
{ "dependencies": { "chart.js": "^4.4.1" } }

React itself is provided by the platform — don't list it. Node-native packages can't run in the browser; put that logic in main.py (with its deps in requirements.txt).

Workflow

  • Scaffold: artifuncs init my-func --custom
  • Web IDE: edit view/view.tsx → save → the preview rebuilds and remounts live. The preview header's Settings button (also ctx.settings.open()) edits the settings declared in artifuncs.json — values feed executions and fire settings.valueChanged.
  • CLI: artifuncs dev syncs your local folder; saving any file under view/ triggers a sandbox rebuild and the open preview hot-reloads.
  • Build errors (syntax error in the view, missing import) appear in the log panel and the preview shows a build-failed state.