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

> Documentation Index
> Fetch the complete documentation index at: https://gistui.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Custom components

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.

### React

```tsx
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} />;
```
### Vue

```vue
<script setup lang="ts">
import { GistChildren, type GistUIComponentProps } from "@gistui/vue";
defineProps<GistUIComponentProps>();
</script>

<template>
  <section class="my-card"><GistChildren /></section>
</template>
```

```vue
<GistUI :source="answer" :components="{ Card: MyCard }" />
```
### Svelte

```svelte
<script lang="ts">
  import { GistChildren, type GistUIComponentProps } from "@gistui/svelte";
  let { props }: GistUIComponentProps = $props();
</script>

<section class="my-card"><GistChildren /></section>
```

```svelte
<GistUI source={answer} components={{ Card: MyCard }} />
```
### Solid

```tsx
import { GistChildren, type GistUIComponentProps } from "@gistui/solid";

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

<GistUI source={answer()} components={{ Card: MyCard }} />;
```
### Vanilla

```ts
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)` | |

```tsx
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.

```tsx
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:

```ts
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:

```ts
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](/docs/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](/docs/guides/security/).

Source: https://gistui.com/docs/guides/custom-components/index.mdx
