Skip to content

Custom components

Replace a built-in component with your own, add new components to the catalog, and have them appear in the model's prompt automatically.

Updated View as Markdown

There are two different jobs here.

Job The model’s prompt Use
Swap a renderer: draw Card with your own component Does not change ui.extend or components
Add a component: teach the model something new Gains one signature line defineComponent + createLibrary

Swap a renderer

The schema stays, so the model writes exactly what it wrote before. Only what is drawn changes.

import type { ComponentProps } from "@gistui/react";
import { ui } from "@gistui/react/ui";

function MyCard({ props, children }: ComponentProps) {
  return <section className="my-card" data-variant={props.v}>{children}</section>;
}

const lib = ui.extend({ Card: MyCard });
<GistUI library={lib} stream={stream} />;
<script setup lang="ts">
import { GistChildren, type GistUIComponentProps } from "@gistui/vue";
defineProps<GistUIComponentProps>();
</script>

<template>
  <section class="my-card"><GistChildren /></section>
</template>
<GistUI :source="answer" :components="{ Card: MyCard }" />
<script lang="ts">
  import { GistChildren, type GistUIComponentProps } from "@gistui/svelte";
  let { props }: GistUIComponentProps = $props();
</script>

<section class="my-card"><GistChildren /></section>
<GistUI source={answer} components={{ Card: MyCard }} />
import { GistChildren, type GistUIComponentProps } from "@gistui/solid";

const MyCard = (p: GistUIComponentProps) => <section class="my-card"><GistChildren /></section>;

<GistUI source={answer()} components={{ Card: MyCard }} />;
import { h } from "@gistui/vanilla";
import { ui } from "@gistui/vanilla/ui";

const lib = ui.extend({
  Card: (ctx) => {
    const el = h("section", { class: "my-card" });
    ctx.place(el); // children go here
    return { el, update: () => true };
  },
});

Your component receives props (what the model wrote, already validated and coerced), node and its children.

Form fields and buttons of your own

A field of your own should still be part of the Form around it: validated by its rules, showing its error, bound to bind:$var. One helper per framework does that.

Framework Field Button
React useGistField(props) useGistButton(props)
Vue useGistField(props)
Svelte gistField(() => p.ctx)
Solid createGistField(props)
Vanilla formField(ctx, onChange)
import { useGistButton, useGistField } from "@gistui/react/ui";

function MyInput(p: ComponentProps) {
  const field = useGistField(p); // name, label, required, error, locked, value, setValue
  return (
    <label>
      {field.label}
      <input name={field.name} value={field.value ?? ""} onChange={(e) => field.setValue(e.target.value)} disabled={field.locked} aria-invalid={!!field.error} />
      {field.error && <small>{field.error}</small>}
    </label>
  );
}

function MyButton(p: ComponentProps) {
  const b = useGistButton(p); // do: actions, submit and draft in forms, opens: dialogs, links
  return <><button {...b.buttonProps}>{b.label}</button>{b.dialog}</>;
}

Render a native control with name (Radix and most libraries render a hidden one), so the form can read it.

Add a new component

Define its schema and its renderer, and build a library that includes it.

import { createLibrary, defineComponent } from "@gistui/react";
import { ui } from "@gistui/react/ui";

const Rating = defineComponent({
  name: "Rating",
  group: "Content",
  description: "star rating, 0 to 5",
  args: ["value"],
  props: {
    value: { type: "number", required: true },
    label: { type: "string" },
    size: { type: "enum", values: ["sm", "md", "lg"], default: "md" },
  },
  component: ({ props }) => <Stars value={props.value} label={props.label} size={props.size} />,
});

const lib = createLibrary({ components: [...ui.components, Rating] });

The prompt now includes Rating(value:num, label:str, size:sm|md|lg) — star rating, 0 to 5, and the parser accepts it. Props can also be a Zod 4 schema or any Standard JSON Schema.

Build the prompt from the same library on the server, so the model and the parser agree:

const system = lib.prompt().text;

Components that take a table

Column-oriented components (a BarChart(labels, Series…) in shadcn or MUI style) can still take one pipe table from the model. Add a table mapping to the spec:

table: { labels: "labels", columns: { component: "Series", label: "category", values: "values", numeric: true } }

BarChart(sales) with sales = |Week|Web|Store then renders exactly as the parts written out, for a fraction of the tokens.

With a component library

Step-by-step pages for shadcn/ui, shadcn-vue, shadcn-svelte, Radix, Reka UI, Bits UI, Kobalte and MUI are in UI libraries.

Checklist

  • Keep the name when you swap, so the prompt does not change.
  • Read only props: they are validated. Do not parse the raw source.
  • Treat every string in props as untrusted text. Do not insert it as HTML.
  • For a URL that loads by itself (an image, a video), give the prop format: "url" so the host policy applies. See Security.
Navigation

Type to search…

↑↓ navigate↵ selectEsc close